库存扣减的那些事

库存扣减的那些事

常见问题

场景

  • 平时我们网购下单,存在一些商品下单后支付成功,还有一些场景可能最后没有支付成功

问题:系统何时减库存

  • 用户下单时扣减?
  • 用户支付完成时扣减?

常见扣减方式

下单减库存

  • 用户下单成功,扣减库存
  • 特征
    • 方式简单,控制最精确,通过数据库事务机制控制商品库存,不会出现超卖
  • 问题
    • 正常情况下,用户下单后一般会很大概率付款,不会有大问题。但是可能会有黄牛刷单的风险,可以做到无风险疯狂占用库存

支付减库存

  • 用户下单后,并不立即扣减,而是用户完成支付后,才真正扣减库存,库存一直留给真正付款的用户
  • 特征
    • 避免黄牛 or 恶意下单
  • 问题
    • 并发高的情况下,用户下单后可能会出现付不了款(超卖问题)
      • 超卖解决办法
        • 商家允许的情况下,通过补货来解决
        • 商家不允许的情况下,提示库存不足,不能完成支付

预扣库存(典型的预扣库存方案)

  • 用户下单后,库存保留一定的时间(比如预售:30分钟),超过时间未付款,库存自动释放,留给其他用户继续购买
  • 特征
    • 下单时,预减库存,检测库存是否有充足
      • 充足,扣减库存
      • 不足,下单失败,提醒用户
    • 支付完成时,校验订单有效性
      • 订单未过期,支付成功
        • 订单过期,其实库存已经释放,给用户退款
  • 问题
    • 虽然设置了库存的保留时间,但是用户可以在释放库存后再次钱柜,或者一次抢购大量库存抢占
    • 并不能彻底解决超卖问题,我们目前都是使用第三方支付,没办法在订单到期释放库存的时候一定完成了付款,在释放库存和支付之间的临界值,假如出现并发,很可能还是导致超卖,但是这种是非常极端的场景
  • 解决办法
    • 结合风控和反作弊的措施来制止,例如:控制购买数量、未付款的情况下不能再次下单等等

      大型秒杀场景

  • 一般秒杀情况下,商品都是爆款(可以理解为”抢到就是赚到”),下单后不付款情况很少,同时商家对于库存有严格的限制,”下单减库存”更加合理,同时该方式对比其他两种场景来说,逻辑上更加简单,性能上占有优势。
  • 如何保障一致性?
    • 根本目的就是保证数据库中库存字段对应的数值不能小于0!
  • 常见解决方案
    • 系统中通过事务来判断,保证扣减后数据不为负数,否则回滚
    • 设置数据库字段数据为无符号证书,通过扣减是,字段值小于零触发sql语句报错
    • update stock set 剩余库存 = 当前剩余库存-扣减库存 where 当前剩余库存-扣减库存 >= 0 实现比乐观锁更乐观的一种控制

先看一个库存扣减的设计

  • 简单描述下图中的设计,先扣减缓存,缓存扣减成功扣减db,若缓存扣减成功,db扣减失败,上锁同步,如果发现库存为0,将缓存设置为-999,防止缓存穿透,如果缓存扣件失败,也直接上锁同步db
  • 如果你觉得这个还不够复杂,等等,你是不是还忽略了一个问题,这个仅仅只是库存扣减,还没有涉及退款,超时等库存的回补操作,这个设计在分布式场景下,复杂度已经很高了

具体问题具体分析

  • 我们在库存扣减设计中核心要解决的问题是什么?
    • 库存扣减环节中,主要解决的问题是”并发写”
  • 写在哪里?
    • 缓存 or 数据库~
  • 缓存中扣减库存
    • 解决的是性能问题,复杂场景的关系映射不能很好支持,但是不支持事务
  • 数据库中扣减

    • 特性
      • 基于Mysql数据存储的特性,同一个sku的库存扣减肯定是在同一行,会有大量的线程竞争InnoDB行锁,并发越高,等待线程就越多,TPS会下降,系统的整体RT就会跟着上升,严重影响到数据库的吞吐量
    • 问题
      • 常见的电商场景中,单个热点商品会影响整个数据库的性能,导致0.01%的商品影响99.99%商品的售卖。
    • 解决办法

库存扣减的核心到底在哪里

  • 通过上面的分析我们可以得出一个结论
    • 在上面举出的复杂例子中,缓存起到的作用,其实跟偏向于做了一层限流,防止流量都压到数据库上
    • 库存扣减的瓶颈不在应用层面,而在于数据库层面

数据库能做哪些优化

  • I: 关闭死锁检测,提高并发处理性能
    对于秒杀/热卖商品这种情况,通过认真的分析,可以得出这种情况的特点,首先是很直接,其次是很暴力,正常的业务死锁会变成超时,最后是不治标,除掉老大,还有老二,问题的症结没有解决,这时候究竟该如何解决呢?经过不断努力,得出了第一种解决方法关掉死锁检测。

  • II:修改源代码,将排队提到进入引擎层前,降低引擎层面的并发度。
    如果请求一股脑的涌入数据库,势必会由于争抢资源造成性能下降,通过排队,让请求从混沌到有序,从而避免数据库在协调大量请求时过载。
    对于在固定的硬件条件下、每个系统都有一个对应的状态最优值,那么在InnoDB的线程数下,将排队队列提到进入引擎层前,这样就能够很好的解决在性能方面具有很好的提高。

  • III:组提交,降低server和引擎的交互次数,降低IO消耗,请求合并:甲买了一个商品,乙也买了同一个商品,与其把甲乙当做当做单独的请求分别执行一次商品库存减一的操作,不如把他们合并后统一执行一次商品库存减二的操作,请求合并的越多,效率提升的就越大。
    根据前面的排队技术,利用多线程并发下,InnoDB内部要做死锁检测等操作,会对性能影响及其严重,明确的串行事务,则server层串行,Group commit减少引擎执行次数,让性能最佳优。

  • IV:目前的一些云上数据库解决方案是一种不错的选择

写在最后

  • Redis只是解决性能问题,数据库才是解决库存一致性问题
  • 没有万能的系统,任何算法和架构都顶不住巨大的流量,限流是万精油!
文章作者: 怀风
文章链接: http://blog.leishunyu.com/2019/11/19/2019-11-19-库存扣减的那些事/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Maple