这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!

标签: 并发 库存 | 发表时间:2020-07-15 15:00 | 作者:前程有光
出处:https://juejin.im/welcome/backend

前言

之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复。今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道一万都不如满满的干货来的实在,干货都下面了!

介绍

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

下单思路

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

(退单有单独的库存流水,申请退单插入流水,退单完成删除流水+还库存)

什么时候进行减库存

  • 方案一:加购时减库存。
  • 方案二:确认订单页减库存。
  • 方案三:提交订单时减库存。
  • 方案四:支付时减库存。

分析

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

所以综上所述: 选择方案三比较合理。

重复下单问题

  1. 用户点击过快,重复提交。
  2. 网络延时,用户重复提交。
  3. 网络延时高的情况下某些框架自动重试,导致重复请求。
  4. 用户恶意行为。

解决办法

  1. 前端拦截,点击后按钮置灰。

  2. 后台: (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 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) {
            //前提 提前将商品库存放入缓存 ,如果缓存不存在,视为没有该商品
            return false;
        }
        //先检查 库存是否充足
        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 的数据同步,减少响应时间
        } else {
            //库存不足,需要增加刚刚减去的库存
            redis.increment(key, num.longValue());
            LogUtil.info("库存不足,并发");
            return false;
        }
        return true;
    }

复制代码

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

综上所述

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

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

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

订单与库存涉及的几个重要知识

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

写在最后的话

大家看完有什么不懂的可以在下方留言讨论,也可以私信问我一般看到后我都会回复的。也欢迎大家关注我的公众号:前程有光,金三银四跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料,文章都会在里面更新,整理的资料也会放在里面。最后觉得文章对你有帮助的话记得点个赞哦,点点关注不迷路,每天都有新鲜的干货分享!

相关 [并发 库存] 推荐:

高并发库存控制

- - 企业架构 - ITeye博客
1、在秒杀的情况下,肯定不能如此高频率的去读写数据库,会严重造成性能问题的. 必须使用缓存,将需要秒杀的商品放入缓存中,并使用锁来处理其并发情况. 当接到用户秒杀提交订单的情况下,先将商品数量递减(加锁/解锁)后再进行其他方面的处理,处理失败在将数据递增1(加锁/解锁),否则表示交易成功. 当商品数量递减到0时,表示商品秒杀完毕,拒绝其他用户的请求.

这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!

- - 掘金后端
之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复. 今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道一万都不如满满的干货来的实在,干货都下面了. 前提:分布式系统,高并发场景 商品A只有100库存,现在有1000或者更多的用户购买.

高并发场景下的订单和库存处理方案,讲的很详细了! - 前程有光 - 博客园

- -
之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复. 今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道一万都不如满满的干货来的实在,干货都下面了. 商品A只有100库存,现在有1000或者更多的用户购买. 如何保证库存在高并发的场景下是安全的.

高并发

- - 开源软件 - ITeye博客
垂直扩展是一种用于增加单个ActiveMQ代理连接数(因而也增加了负载能力)的技术.默认情况下,. ActiveMQ的被设计成尽可高效的传输消息以确保低延迟和良好的性能. 默认情况下,ActiveMQ使用阻塞IO来处理传输连接,这种方式为每一个连接分配一个线程. 你可以为ActiveMQ代理使用非阻塞IO(同时客户端可以使用默认的传输)以减少线程的使用.

并发导论

- - 并发编程网 - ifeve.com
由于之前工作中的疏忽,在使用Java多线程并发的时候出了问题,遂决心全面学习并发相关知识. 写作本文的意图只是希望在写作过程中把想不清楚或是一时无法掌握的地方反复揣摩记录下来. 写作本文参考的各种资料较多,抱歉的是文末的参考文献中对一些叫不上名字或没有出处的资料文献并未列举出来. 由于本人是初入职场的菜鸟,更是并发的门外汉,文中关于并发以及其他软硬件、程序设计语言的论据也许不够客观甚至不够正确.

J.U.C并发框架

- - 并发编程网 - ifeve.com
作者:Doug Lea. 在J2SE1.5中,java.util.concurrent包下的大部分同步工具(锁、屏障等)以AbstractQueuedSynchronizer类为基础来构建. 这个框架提供了一些常用机制用于自动管理并发状态、阻塞及非阻塞线程,以及队列. 本论文描述了该框架的根源、设计、实现、用法及性能.

高并发---限流

- - Java - 编程语言 - ITeye博客
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流. 缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流.

tomcat 高并发优化

- - 企业架构 - ITeye博客
maxThreads:tomcat起动的最大线程数,即同时处理的任务个数,默认值为200. minSpareThreads 表示空闲的线程数,据我的理解,类似于连接池. acceptCount:当tomcat起动的线程数达到最大时,接受排队的请求个数,默认值为100.    另外,有时候,还需要调整jvm的启动参数.

应对高并发攻击

- - 开源软件 - ITeye博客
近半个月过得很痛苦,主要是产品上线后,引来无数机器用户恶意攻击,不停的刷新产品各个服务入口,制造垃圾数据,消耗资源. 他们的最好成绩,1秒钟 可以并发6次,赶在Database入库前,Cache进行Missing Loading前,强占这其中十几毫秒的时间,进行恶意攻击. 为了应对上述情况,做了如下调整:.

Java并发编程基础

- - 并发编程网 - ifeve.com
并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力. 如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互性将大大改善. 现代的PC都有多个CPU或一个CPU中有多个核. 是否能合理运用多核的能力将成为一个大规模应用程序的关键. 进程是以独立于其他进程的方式运行的,进程间是互相隔离的.