前言

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

介绍

前提:分布式系统,高并发场景
商品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<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) {            //前提 提前将商品库存放入缓存 ,如果缓存不存在,视为没有该商品            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)批改 + 弥补机制

写在最初的话

大家看完有什么不懂的能够在下方留言探讨,也能够私信问我个别看到后我都会回复的。最初感觉文章对你有帮忙的话记得点个赞哦,点点关注不迷路,每天都有陈腐的干货分享!