前言

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

先容

条件:分布式系统,高并发场景
商品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)修改 + 抵偿机制

写在最后的话

人人看完有什么不懂的可以在下方留言讨论,也可以私信问我一样平常看到后我都市回复的。也迎接人人关注我的民众号:前途有光,金三银四跳槽面试季,整理了1000多道快要500多页pdf文档的Java面试题资料,文章都市在内里更新,整理的资料也会放在内里。最后以为文章对你有辅助的话记得点个赞哦,点点关注不迷路,天天都有新鲜的干货分享!