WES 重构系列(八):WES 三级库存模型
文内的图示需要科学上网才能流畅查看,图床直接用了 GitHub。
一、WES 的多级库存模型是怎样的?
WES 的库存模型分为三层:
- 一级库存
- 二级库存
- 三级库存
粒度逐件变细。一级库存是全仓库存,商品维度,二级库存是库区+商品+批次+包装,三级库存则细粒度到库位(料箱、料格),与具体的存储位直接关联。

二、一级库存的优势与弊端?
先说优势,对于全仓库存的可视化,其实是简单了许多,其次上游调取库存快照时,减少了计算。
劣势也很明显:
- 一级库存是粒度较大,如果是大库存商品并且是热点商品的话,即使是在 B 端也很容易变成热点行,高并发下,出现性能问题。
- 库存的级数越多,要保证数据一致性的难度就越高,代价也越高。
- 死锁的风险也会增大

三、重构中如何做取舍?
工作越久,越觉得软件开发中,没有银弹,方案本身没有好坏,只有适合与不适合,大多数时候我们都在做权衡(Trade-Off)。
对于 WES 来说,数据一致性和稳定性比报表类的需求其实是更核心的,一级库存固然有一定优点,但都可以通过二级库存聚合计算得到,而且一级库存的使用场景都很低频,去掉一级库存完全是能满足业务需求的。
去掉一级库存的优势就很明显了,数据一致性的维护更简单了,死锁风险降低,代码层面也会降低复杂度,热点行的发生频率也会下降一个级数。
所以最终还是去掉了一级库存。
四、为什么要有二级库存?
二级库存关注的维度是单库区商品批次维度,那我们先假设去掉二级库存,那会有哪些问题?
- 性能: 在一些需要库区视角的场景中,比如出库单缺货时无法快速判断,影响补货时效,因为三级库存是分散在具体库位的,需要去三级库存表查找目标库存列表,并做聚合计算,尤其在某些仓储场景下,sku 种类很少,那要查询的三级库存数量会很大,对三级库存表是一个很大的负担。
- 逻辑耦合:库存决策(是否满足订单)与执行细节(从哪个货位取货)混杂,增加代码复杂度
- 另外二级库存与我们的出库调度模型有关联,减少二级库存会让上游的出库单与调度任务直接关联,会缺少灵活性。
基于上述原因,我们最终还是保留了二级库存。

五、库存扣减的幂等性如何保证?
我们有库存流水表,会有幂等键,二级库存是 出库作业类型 + 作业单 ID + 明细 ID,三级库存扣减是出库作业类型 + 任务 ID。
六、MySQL 库存扣减的性能瓶颈与优化思路?
6.1 性能瓶颈

MySQL 库存扣减其实本质上是热点行更新的问题,当多个并发事务同时尝试更新同一行热点数据时,可能会导致锁竞争和冲突。这会增加事务的等待时间和冲突概率,导致性能下降,并可能引发死锁问题。
具体来说,可能会产生以下问题(参考了网络上的内容,学习一下):
- 锁竞争,热点数据的更新是通过update语句进行的,而update是需要给记录增加排他锁的,这就会导致大量的请求被阻塞。降低整个系统的吞吐量。
- 占用数据库连接,当有大量的update语句,因为要修改同一条记录而被阻塞的时候,他们持有的数据库连接是不会释放的,而数据库连接又是有限的,所以会导致连接数不够,进而影响整个系统的吞吐量及可用性。
- 耗尽数据库CPU,大量锁等待,就会导致大量的自旋,多个线程就会不断的尝试获取锁,CPU就需要不断的执行自旋操作,并且需要做死锁检测,消耗大量CPU时间。并且在这个过程中,操作系统也需要频繁的进行线程上下文的切换,这个过程会导致CPU时间片的浪费。
- 死锁风险,在高并发的情况下。由于数据库需要频繁定位和更新这些特定行,可能会增加锁竞争和死锁的风险,影响并发性能。
- 索引维护开销大,频繁地更新热点数据,不仅会导致数据的变化,还可能导致相关索引的频繁维护,这可能会增加数据库的开销,导致性能下降。
- 主从不一致,热点数据的频繁更新,如果在主从复制出现延迟的情况下,就会放大数据不一致的概率。
6.2 优化思路
这类问题的解决思路可以分为3类,分别是排队、拆分以及批次执行。
思路 一:排队
排队方案要么是加锁,要么是单线程执行。
如果能使用Redis的话,那么就可以利用他的高并发、单线程特点来解决这个问题。
如果是使用数据库的话,可以使用优化版的云MySQL,腾讯云 MySQL 和阿里云 MySQL。
他们都做了二次开发优化,系统会自动探测是否有单行的热点更新,让同一个热点行的更新语句,在执行层进行排队。这样的排队相比update的排队,要轻量级很多,因为他不需要自旋,不需要抢锁。
思路二:拆分(用得不多)
类似分段锁机制,将一次扣减分散到不同的库表中进行,但可能存在碎片库存。
思路三:合并
把多个UPDATE合成一个UPDATE, 比如一个用户,有10个占用库存请求,每次占用1个,那么就可以提供一个批量占用的接口,让上游一次性把10个占用合并一起,这样数据库只需要做一次更新就行了
局限性:不是所有请求都可以合并的,有些场景,如电商的秒杀,用户需要很快的知道反馈,而批量执行就需要有个窗口来聚合,用户是不能接受这种等待窗口的。在一些异步链路上,可以用这种方案。

七、学习互联网电商高并发库存扣减的思路
7.1 两种方案
主流的有两种方案:
- 数据库扣减:
- redis 扣减:使用lua 脚本,先判断库存呢是否足够,足够再扣减
7.2 Redis 库存的正确扣减
秒杀因为是一个高频的并发库存扣减的场景,所以,如何提升库存扣减的性能,并且保证他的准确性,这是一个在秒杀业务中极其重要的课题,稍有不慎就会带来超卖、少卖等问题。另外还需要关注一下对账的系统设计。
具体的方案设计如下图:

7.3 Redis保存库存的时候,如何避免被Redis清理掉?
核心其实是 Redis 的内存淘汰策略。
用 volatile 相关策略,这样只有设置了超时时间的才可能被淘汰。
阿里云上的Redis的默认的淘汰策略是volatile-lru。
腾讯云默认是noeviction。