电商平台库存扣减设计思路


业务场景

一般来说,电商平台涉及到减库存的场景为:提交订单--收银台支付,这里会有减库存时机问题,主流使用第三种方案。

  • 下单减库存。即提交订单后就用商品总库存-订单库存数量。用事务控制订单生成和库存更新,不会存在超卖问题。但是这里有个问题,下单后并不一定付款,如果存在恶意刷单会影响正常交易,且事务内生成订单且更新库存,业务量大会有性能问题。
  • 付款减库存。提交订单后,并不扣减库存,直到支付成功后真正扣减库存。但是这里也有个问题,成功下单的用户,到支付时没有库存可用,导致交易失败。
  • 预扣库存。提交订单后,库存保留一定时间,比如10分钟,超过这个时间库存释放。付款时真正完成库存扣减。这种方案并未完全解决超卖和刷单问题

库存扣减

库存扣减准确,支持高并发,满足高可用性(这个可以从保证整条链路上不存在单点,做兜底方案考虑,这里不做重点讨论)

这里需要关注以下点

  • 库存剩余数量需大于扣减数量,库存不能扣成负数
  • 扣减多条sku,需要保证原子性,一条扣减失败,全部回滚
  • 有库存扣减才能加库存
  • 保证幂等性

 

实现方案

实现思路总体分为两大类,第一种是基于Mysql,另一种基于缓存实现比如Redis

1.基于缓存实现

适合于高流量,对库存准确性要求不是非常高的场景下使用。

利用redis的incrby特性扣减库存。但是需要考虑缓存丢失恢复场景。举例一个发奖场景,Redis初始库存 = 总库存数量 - 已发放奖励数量,如果使用异步发奖,需要等待MQ中发奖消息消费完毕才能重新初始化Redis库存。否则会有不一致问题。

  • 使用lua实现扣减逻辑
  • 分布式环境下需使用分布式锁控制只有一个服务初始化库存,且需要注意初始化时机。
  • 不具备数据库事务特性(比如批量扣减,只能将扣减的sku打包在lua脚本内,lua脚本循环做扣减,虽然保证原子性但是中途扣减失败无法回滚。假如最后一条sku扣减失败,结果返回失败,但是之前sku扣减成功的无法回滚)所以需要有对账定时任务,定期保证最终一致性。
  • 可以采用redis集群模式分摊数据
static {
        /**
         *
         * @desc 扣减库存Lua脚本
         * 库存(stock)-1:表示不限库存
         * 库存(stock)0:表示没有库存
         * 库存(stock)大于0:表示剩余库存
         *
         * @params 库存key
         * @return
         *   -3:库存未初始化
         *   -2:库存不足
         *   -1:不限库存
         *   大于等于0:剩余库存(扣减之后剩余的库存)
         *   redis缓存的库存(value)是-1表示不限库存,直接返回1
         */
        StringBuilder sb = new StringBuilder();
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        sb.append("    local num = tonumber(ARGV[1]);");
        sb.append("    if (stock == -1) then");
        sb.append("        return -1;");
        sb.append("    end;");
        sb.append("    if (stock >= num) then");
        sb.append("        return redis.call('incrby', KEYS[1], 0 - num);");
        sb.append("    end;");
        sb.append("    return -2;");
        sb.append("end;");
        sb.append("return -3;");
        STOCK_LUA = sb.toString();
    }

2.基于数据库实现

 1)方案1 

  • 基于乐观锁保证并发扣减下数据正确性
  • 基于事务特性,保证批量扣减下,部分扣减失败,全部回滚 
  • 库存扣减和库存流水在同一个事务内
  • 库存流水需要记录业务流水号,当库存归还场景下需要携带扣减业务流水号。
  • 需要幂等控制

          

    

//避免扣成负数
update
inventory set leaved_amount = leaved_amount - #{count} where sku_id='123' and leaved_amount >= #{count}

2)方案2

        

 

     使用这种方案可以很大程度缓解库存校验和查询库存时的性能问题,但会带来库存实时性的问题,即redis和mysql主节点一致性问题。

   

    3)总结:

  使用数据库方案简单高效,基于数据库ACID特性很容易保证不出现超卖和并发下库存准确性。

  缺点:性能瓶颈在mysql主节点的写入,

     


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM