高并发下的订单和库存处理

2023-11-07

介绍
前提:分布式系统,高并发场景
商品A只有100库存,现在有1000或者更多的用户购买。如何保证库存在高并发的场景下是安全的。
预期结果:1.不超卖 2.不少卖 3.下单响应快 4.用户体验好

下单思路:

下单时生成订单,减库存,同时记录库存流水,在这里需要先进行库存操作再生成订单数据,这样库存修改成功,响应超时的特殊情况也可以通过第四步定时校验库存流水来完成最终一致性。
支付成功删除库存流水,处理完成删除可以让库存流水数据表数据量少,易于维护。
未支付取消订单,还库存+删除库存流水
定时校验库存流水,结合订单状态进行响应处理,保证最终一致性
(退单有单独的库存流水,申请退单插入流水,退单完成删除流水+还库存)

什么时候进行减库存
方案一:加购时减库存。
方案二:确认订单页减库存。
方案三:提交订单时减库存。
方案四:支付时减库存。
分析:

方案一:在这个时间内加入购物车并不代表用户一定会购买,如果这个时候处理库存,会导致想购买的用户显示无货。而不想购买的人一直占着库存。显然这种做法是不可取的。唯品会购物车锁库存,但是他们是另一种做法,加入购物车后会有一定时效,超时会从购物车清除。
方案二:确认订单页用户有购买欲望,但是此时没有提交订单,减库存会增加很大的复杂性,而且确认订单页的功能是让用户确认信息,减库存不合理,希望大家对该方案发表一下观点,本人暂时只想到这么多。
方案三:提交订单时减库存。用户选择提交订单,说明用户有强烈的购买欲望。生成订单会有一个支付时效,例如半个小时。超过半个小时后,系统自动取消订单,还库存。
方案四:支付时去减库存。比如:只有100个用户可以支付,900个用户不能支付。用户体验太差,同时生成了900个无效订单数据。
所以综上所述:
选择方案三比较合理。

重复下单问题

用户点击过快,重复提交。
网络延时,用户重复提交。
网络延时高的情况下某些框架自动重试,导致重复请求。
用户恶意行为。
解决办法
前端拦截,点击后按钮置灰。
后台:
(1)redis 防重复点击,在下单前获取用户token,下单的时候后台系统校验这个 token是否有效,导致的问题是一个用户多个设备不能同时下单。

//key , 等待获取锁的时间 ,锁的时间
redis.lock("shop-oms-submit" + token, 1L, 10L);

redis的key用token + 设备编号 一个用户多个设备可以同时下单。

   //key , 等待获取锁的时间 ,锁的时间
    redis.lock("shop-oms-submit" + token + deviceType, 1L, 10L);

(2)防止恶意用户,恶意攻击 : 一分钟调用下单超过50次 ,加入临时黑名单 ,10分钟后才可继续操作,一小时允许一次跨时段弱校验。使用reids的list结构,过期时间一小时

/**
     * @param token
     * @return true 可下单
     */
    public boolean judgeUserToken(String token) {
        //获取用户下单次数 1分钟50次
        String blackUser = "shop-oms-submit-black-" + token;
        if (redis.get(blackUser) != null) {
            return false;
        }
        String keyCount = "shop-oms-submit-count-" + token;
        Long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
        //每一小时清一次key 过期时间1小时
        Long count = redis.rpush(keyCount, String.valueOf(nowSecond), 60 * 60);
        if (count < 50) {
            return true;
        }
        //获取第50次的时间
        List<String> secondString = redis.lrange(keyCount, count - 50, count - 49);
        Long oldSecond = Long.valueOf(secondString.get(0));
        //now > oldSecond + 60 用户可下单
        boolean result = nowSecond.compareTo(oldSecond + 60) > 0;
        if (!result) {
            //触发限制,加入黑名单,过期时间10分钟
            redis.set(blackUser, String.valueOf(nowSecond), 10 * 60);
        }
        return result;
    }

如何安全的减库存

多用户抢购时,如何做到并发安全减库存?

方案1: 数据库操作商品库存采用乐观锁防止超卖:

**sql:update sku_stock set stock = stock - num where sku_code = '' and stock - num > 0;**

分析:
高并发场景下,假设库存只有 1件 ,两个请求同时进来,抢购该商品.
数据库层面会限制只有一个用户扣库存成功。在并发量不是很大的情况下可以这么做。但是如果是秒杀,抢购,瞬时流量很高的话,压力会都到数据库,可能拖垮数据库。

方案2:利用Redis单线程 强制串行处理

/**
     * 缺点并发不高,同时只能一个用户抢占操作,用户体验不好!
     *
     * @param orderSkuAo
     */
    public boolean subtractStock(OrderSkuAo orderSkuAo) {
        String lockKey = "shop-product-stock-subtract" + orderSkuAo.getOrderCode();
        if(redis.get(lockKey)){
            return false;
        }
        try {
            lock.lock(lockKey, 1L, 10L);
            //处理逻辑
        }catch (Exception e){
            LogUtil.error("e=",e);
        }finally {
            lock.unLock(lockKey);
        }
        return true;
    }

分析:
利用Redis 分布式锁,强制控制同一个商品处理请求串行化,缺点并发不高 ,处理比较慢,不适合抢购,高并发场景。用户体验差,但是减轻了数据库的压力。

方案3 :redis + mq + mysql 保证库存安全,满足高并发处理,但相对复杂。

 /**
 * 扣库存操作,秒杀的处理方案
 * @param orderCode
 * @param skuCode
 * @param num
 * @return
 */
public boolean subtractStock(String orderCode,String skuCode, Integer num) {
    String key = "shop-product-stock" + skuCode;
    Object value = redis.get(key);
    if (value == null) {
         // 去查数据库的数据
         // 并且把数据库的库存set进redis,注意使用NX参数表示只有当没有redis中没有这个key的时候才set库存数量到redis
         //注意要设置序列化方式为StringRedisSerializer,不然不能把value做加减操作
         // 同时设置超时时间,因为不能让redis存着所有商品的库存数,以免占用内存
        if (count >=0) {
            //设置有效期十分钟
            redisTemplate.expire(key, 60*10+随机数防止雪崩, TimeUnit.SECONDS);
        }
    }
    //先检查 库存是否充足
    Integer stock = (Integer) value;
    if (stock < num) {
        LogUtil.info("库存不足");
        return false;
    } 
   //不可在这里直接操作数据库减库存,否则导致数据不安全
   //因为此时可能有其他线程已经将redis的key修改了
    //redis 减少库存,然后才能操作数据库
    Long newStock = redis.increment(key, -num.longValue());
    //库存充足
    if (newStock >= 0) {
        LogUtil.info("成功抢购");
        //TODO 真正扣库存操作 可用MQ 进行 redis 和 mysql 的数据同步,减少响应时间
        // update 数据库中商品库存和订单系统下单,单的状态未待支付
        // 分开两个系统处理时,可以用LCN做分布式事务,但是也是有概率会订单系统的网络超时
       // 也可以使用最终一致性的方式,更新库存成功后,发送mq,等待订单创建生成回调。
            boolean res= updateProduct(req);
              if (res)
                createOrder(req);
    } else {
        //库存不足,需要增加刚刚减去的库存
        redis.increment(key, num.longValue());
        LogUtil.info("库存不足,并发");
        return false;
    }
    return true;
}

分析:
利用Redis increment 的原子操作,保证库存安全,利用MQ保证高并发响应时间。但是事需要把库存的信息保存到Redis,并保证Redis 和 Mysql 数据同步。缺点是redis宕机后不能下单。
increment 是个原子操作。

update使用乐观锁
updateProduct方法中执行的sql如下:

update Product set count = count - #{购买数量} where id = #{id} and count - #{购买数量} >= 0;

虽然redis已经防止了超卖,但是数据库层面,为了也要防止超卖,以防redis崩溃时无法使用或者不需要redis处理时,则用乐观锁,因为不一定全部商品都用redis。

利用sql每条单条语句都是有事务的,所以两条sql同时执行,也就只会有其中一条sql先执行成功,另外一条后执行,也如上文提及到的场景一样。

综上所述:
方案三满足秒杀、高并发抢购等热点商品的处理,真正减扣库存和下单可以异步执行。在并发情况不高,平常商品或者正常购买流程,可以采用方案一数据库乐观锁的处理,或者对方案三进行重新设计,设计成支持单订单多商品即可,但复杂性提高,同时redis和mysql数据一致性需要定期检查。

订单时效问题
超过订单有效时间,订单取消,可利用MQ或其他方案回退库存。

设置定时检查
Spring task 的cron表达式定时任务
MQ消息延时队列

订单与库存涉及的几个重要知识
TCC 模型:Try/Confirm/Cancel:不使用强一致性的处理方案,最终一致性即可,下单减库存,成功后生成订单数据,如果此时由于超时导致库存扣成功但是返回失败,则通过定时任务检查进行数据恢复,如果本条数据执行次数超过某个限制,人工回滚。还库存也是这样。
幂等性:分布式高并发系统如何保证对外接口的幂等性,记录库存流水是实现库存回滚,支持幂等性的一个解决方案,订单号+skuCode为唯一主键(该表修改频次高,少建索引)
乐观锁:where stock + num>0
消息队列:实现分布式事务 和 异步处理(提升响应速度)
redis:限制请求频次,高并发解决方案,提升响应速度
分布式锁:防止重复提交,防止高并发,强制串行化
分布式事务:最终一致性,同步处理(Dubbo)/异步处理(MQ)修改 + 补偿机制

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

高并发下的订单和库存处理 的相关文章

  • Java并发编程:线程池的使用

    https www cnblogs com dolphin0520 p 3932921 html Java并发编程 线程池的使用 在前面的文章中 我们使用线程的时候就去创建一个线程 这样实现起来非常简便 但是就会有一个问题 如果并发的线程数
  • 队列的使用注意点

    队列通常使用链表或数组作为元素的基础存储 队列的大小需要约束 如果允许内存中的队列不受限制 那么对于许多类别的问题 它可以不受限制地增长 直到它达到灾难性失败的地步 因为它耗尽了内存 这发生在生产者超过消费者的时候 无界队列在系统中可能很有
  • JUC AQS ReentrantLock源码分析(一)

    Java的内置锁一直都是备受争议的 在JDK 1 6之前 synchronized这个重量级锁其性能一直都是较为低下 虽然在1 6后 进行大量的锁优化策略 但是与Lock相比synchronized还是存在一些缺陷的 虽然synchroni
  • 互斥和同步-读者/写者问题

    读者 写者问题 问题定义 存在一个多进程共享的数据区 该数据区可以是 一个文件或者一块内存或者一组寄存器 有些进程reader只读取该数据区中的数据 有些进程writer只往数据区写数据 满足条件 任意数量的读进程可以同时读该文件 一次只有
  • java 限流策略

    概要 在大数据量高并发访问时 经常会出现服务或接口面对暴涨的请求而不可用的情况 甚至引发连锁反映导致整个系统崩溃 此时你需要使用的技术手段之一就是限流 当请求达到一定的并发数或速率 就进行等待 排队 降级 拒绝服务等 在限流时 常见的两种算
  • java并发总结

    一 并发基础 1 进程与线程 进程 程序由指令和数据组成 但这些指令要运行 数据要读写 就必须将指令加载至 CPU 数据加载至内存 在指令运行过程中还需要用到磁盘 网络等设备 进程就是用来加载指令 管理内存 管理 IO 的 当一个程序被运行
  • Unsafe初探

    Unsafe Unsafe 是sun misc Unsafe下的一个包 通过这个类可以直接使用底层native方法来获取和操作底层的数据 例如获取一个字段在内存中的偏移量 利用偏移量直接获取或修改一个字段的数据等等 当然这个类正如他的名字一
  • iOS并发编程(一)-简介

    一个菜鸟的自我修养 就是在低级职位上不抓狂 当一个优秀的菜鸟 就是为了有一天不当菜鸟 瞅准机会迅速脱离菜鸟轨道 然后一路飞翔到世界的尽头 接下来系统的学习下并发编程 会有几篇吧 不多说 走起 简介 1 异步设计方式 传统并发编程模型是线程
  • Java中synchronized同步锁用法及作用范围

    Java 中的 synchronized 关键字可以在多线程环境下用来作为线程安全的同步锁 本文主要对 synchronized 的作用 以及其有效范围进行讨论 Java中的对象锁和类锁 java的对象锁和类锁在锁的概念上基本上和内置锁是一
  • 无锁和偏向锁有什么区别?

    无锁和偏向锁是 Java 中的两种轻量级锁实现 它们和重量级锁相比 具有更高的性能和更低的开销 它们之间的区别如下 无锁 CAS 自旋锁 无锁是一种不需要使用锁的同步技术 它的实现依赖于 CAS Compare And Swap 操作 通过
  • 合理配置线程池核心线程数(IO密集型和CPU密集型)

    1 代码查看服务器的核心数 要合理配置线程数首先要知道公司服务器是几核的 代码查看服务器核数 System out println Runtime getRuntime availableProcessors 2 合理线程数配置之CPU密集
  • 并发策略之分工原则

    本文主要思想来自 Java虚拟机并发编程 薛笛 译 为什么要用并发 并发是再在有限的资源下提高性能的有效手段 当然现在互联网环境下并发访问的现象也比比皆是 但是本文并不涉及处理并发访问 而是使用并发手段解决复杂任务的策略 另外关于并发和并行
  • java并发的基本概念和级别

    之前买了一本实战Java高并发程序设计 这里记一下笔记 至于书怎么样 读完之后再看值不值得推荐 先提供下试读pdf的 下载地址 关于java并发的一些概念 并发的概念 并发 Concurrency 和并行 Parallelism 并发偏重于
  • Java并发编程之CyclicBarrier详解

    简介 栅栏类似于闭锁 它能阻塞一组线程直到某个事件的发生 栅栏与闭锁的关键区别在于 所有的线程必须同时到达栅栏位置 才能继续执行 闭锁用于等待事件 而栅栏用于等待其他线程 CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集
  • Java线程池的使用(简单实现)

    一 线程池的概念 创建Java线程需要给线程分配堆栈内存以及初始化内存 还需要进行系统调用 频繁地创建和销毁线程会大大降低系统的运行效率 采用线程池来管理线程有以下好处 提升性能 线程池能独立负责线程的创建 维护和分配 线程管理 每个Jav
  • Semaphore 源码分析

    需要提前了解的知识点 AbstractQueuedSynchronizer 实现原理 类介绍 Semaphore 信号量 是用来控制同时访问特定资源的线程数量 它通过协调各个线程 以保证合理的使用公共资源 比如控制用户的访问量 同一时刻只允
  • 并发问题(二)什么是并发

    1 什么是并发操作 并发操作是指同一时间可能有多个用户对同一数据进行读写操作 2 并发操作对数据的影响 如果对并发操作不做任何控制的话 会造成数据的不完整性 可能造成读脏数据 不可重复读 丢失修改还有幻读 3 对数据不完整性的举例 1 丢失
  • fcgi程序两种编写风格

    fcgi进程可以写成单线程的 也可以写成多线程的 单线程就是main函数中有一个死循环 一直等待接受请求 有请求过来时 就处理请求 并返回结果 没有并发性 多线程也分两种模式 一种是main函数起多个线程 每个线程都独立接受请求 另一种是m
  • MySQL最常用的二种存储引擎MyISAM和InnoDB的介绍

    1 MyISAM 默认表类型 它是基于传统的ISAM类型 ISAM是Indexed Sequential Access Method 有索引的顺序访问方法 的缩写 它是存储记录和文件的标准方法 不是事务安全的 而且不支持外键 如果执行大量的
  • 并发编程----4.java并发包中线程池的原理研究

    并发编程 4 java并发包中线程池的原理研究 java并发包中线程池ThreadPoolExecutor的原理研究 线程池的优点 线程的复用 减少线程创建和销毁带来的消耗 提供了一种资源限制和线程管理的手段 比如限制线程的个数和动态新增线

随机推荐

  • 2020“闭关”跳槽季,啃透分布式三大技术:限流、缓存、通讯

    01 分布式限流 1 1 Nginx ZooKeeper面试常备题 附答案 请解释一下什么是 Nginx 请列举 x Nginx 的一些特性 请列举 x Nginx 和 和 Apache 之间的不同点 请解释 x Nginx 如何处理 P
  • el-input获取输入框光标位置

    今天接到需求 输入框在正常输入的同时 可以通过点击其他按钮在输入框光标位置添加内容 那么这时候就需要去获取输入框的光标内容 由于在点击其他按钮时 输入框会自动触发失焦事件 因此在blur的时候去触发方法即可
  • Linux-安装MySQL(详细教程)

    Linux 安装MySQL 前言 一 概述 二 下载 三 安装 四 卸载 五 常用设置 六 可能遇到的问题 前言 本文的主要内容是在 Linux 上安装 MySQL 以下内容是源于 B站 MySQL数据库入门到精通 整理而来 一 概述 My
  • QVariant的使用

    一 介绍 QT的官方文档这么写的 The QVariant class acts like a union for the most common Qt data types QVariant可以存储各种数据类型 QVariant行为类似于
  • 长整数相乘

    include
  • JS中的async/await的用法和理解

    1 首先需要理解async 和 await的基本含义 async 是一个修饰符 async 定义的函数会默认的返回一个Promise对象resolve的值 因此对async函数可以直接进行then操作 返回的值即为then方法的传入函数 0
  • Python技能树

    Python技能树 44条消息 三元表达式 进阶语法 CSDNPython技能树 输出偶数个数 不使用三元组 even count 1 if i 2 0 else 0 使用嵌套的三元组表达式统计数字频率 如果是2的倍数加1 如果是4的倍数加
  • python医学科研中能做什么-科研画图都用什么软件?

    作为一只理工狗 我们不仅可能需要熬夜编程 更需要在很多时候画图来展示自己的结果 如果不能用漂亮的图片来展示结果 别人对你的工作评价也许会大打折扣 这样熬夜编的程基本上算是白熬了 下面隆重向大家推荐十款主流画图软件 美好的生活从作出高品 bi
  • 实现一个vue3组件库 - scrollbar滚动条

    theme vue pro 前言 思来想去很久 我都不知道该最先介绍哪一个组件才好 虽然我写的第一个组件是button按钮 但是也是因为简单所以第一个写 逻辑代码不是很多 样式倒是一大堆 感觉不适合用作开篇介绍 最后选择了scrollbar
  • matlab练习程序(最大似然估计)

    clear all close all clc randn seed 0 一维情况 mu 0 N 100000 S 5 data mvnrnd mu S N me mean data S2 1 N sum data me 2 二维或多维情况
  • idea下方控制台显示多个springboot项目

    1 在idea打开View gt Tool Windows gt Services 2 添加服务 选择Spring Boot 3 效果图
  • ChatGPT的前世今生——混沌初开

    目录 ChatGPT的前世今生 混沌初开 ChatCPT简介 ChatCPT是什么 ChatCPT的火爆程度 ChatCPT火爆的原因 1 功能强大 应用范围广泛 2 训练数据量大 模型效果好 3 优秀的商业模式 OpenAI公司 公司创始
  • HTML5 canvas标签-2 简单的3种滤镜

    在发现canvas有这么多功能后 我首先尝试着去做一些滤镜 最基本的就是胶片 这个在w3school上有demo 假设原本颜色为rgb r g b 只需要将它变成rgb 255 r 255 g 255 b 即可 原图处理后的 for var
  • Layui项目实战

    使用语言 C Js Html 使用框架 MVC Layui 使用插件 JQuery Layui 一 Layui父窗体前端代码 1 Html代码 div class layui col md12 style padding 8px div c
  • (Linux无线网卡WIFI上网 一 )USB-WIFI驱动移植

    导航 Linux无线网卡WIFI上网 一 USB WIFI驱动移植 Linux无线网卡WIFI上网 二 WPA SUPPLICANT Linux下的wifi管理工具移植 Linux无线网卡WIFI上网 三 嵌入式Linux下的WIFI使用
  • 常见十四种的Java算法

    一 简单列出常见的Java中14种算法 序号 简称 英文 简介 1 二分查找法 Binary Search 二分查找要求线性表必须采用顺序存储结构 而且表中元素按关键字有序排列 2 冒泡排序算法 Bubble Sort 它重复地走访过要排序
  • python搭建ip池

    在爬取网站的时候我们有时候会遭受封ip等显现 因此我们需要搭建自己的ip池用于爬虫 代码过程简述 1 爬取代理ip网站信息 2 将获取的信息处理得到ip等关键信息 3 保存首次获取的ip信息并检测其是否可用 4 检测完毕将可用ip保存 搭建
  • 操作系统重点_重点:运动系统

    操作系统重点 The Locomotion System for Unity has previously been briefly mentioned on this blog but this post will go more in
  • 前端自定义网页鼠标右键菜单

    监听全局上下文菜单 document addEventListener contextmenu function e 阻止默认 e preventDefault 自定义鼠标右键菜单栏 console log 鼠标右击
  • 高并发下的订单和库存处理

    介绍 前提 分布式系统 高并发场景 商品A只有100库存 现在有1000或者更多的用户购买 如何保证库存在高并发的场景下是安全的 预期结果 1 不超卖 2 不少卖 3 下单响应快 4 用户体验好 下单思路 下单时生成订单 减库存 同时记录库