订单和库存处理方案

介绍

前提:分布式系统,高并发场景
商品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 uid
     * @return true 可下单
     */
    public boolean judgeUserCanContinue(long uid) {
        try {
            //获取用户下单次数 1分钟20次
            log.info("用户分钟下单次数防刷校验 uid={}", uid);
            String blackUser = "XXXX-pay-black-" + uid;
            //判断用户是否已被限制
            if (jedisExtOperator.get(blackUser) != null) {
                log.error("用户分钟下单次数防刷校验,用户已被限制 uid={}", uid);
                return false;
            }
            String keyCount = "XXXX-pay-count-" + uid;
            Long nowSecond = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
            Long count = redis.rpush(keyCount, String.valueOf(nowSecond));
            //设置过期时间 2分钟
            redis.expire(keyCount, 2 * 60);
            log.info("用户分钟下单次数防刷校验,目前缓存中总次数 count={} uid={}", count, uid);
            if (count < 20) {
                //不足1分钟20次直接返回
                return true;
            }
            //获取第20次的时间
            String secondString = redis.lindex(keyCount, -19);
            log.info("用户分钟下单次数防刷校验 uid={} , 第20次时间:{}秒, 当前时间:{}秒 ", uid, secondString, nowSecond);
            Long oldSecond = Long.valueOf(secondString);
            //now - oldSecond > 60 用户可下单
            boolean result = nowSecond - oldSecond > 60;
            if (!result) {
                //触发限制,加入黑名单,过期时间10分钟
                log.info("用户分钟下单次数防刷校验 触发限制,加入黑名单,过期时间10分钟 uid={} , 第20次时间:{}秒, 当前时间:{}秒 ", uid, secondString, nowSecond);
                redis.setex(blackUser, 10 * 60, String.valueOf(nowSecond));
            }
            return result;
        } catch (Exception e) {
            log.error("用户支付单防刷异常 uid={}", uid, e);
        }
        return true;
    }

如何安全的减库存
多用户抢购时,如何做到并发安全减库存?
方案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)修改 + 补偿机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345