目录
- 一、前言:热点库存扣减的挑战
- 二、问题分析:传统Redis分桶方案的三大缺陷
- [三、核心思路:Redis做计数 + DB明细为准](#三、核心思路:Redis做计数 + DB明细为准)
- 四、方案详解:四大核心模块
- 五、防超卖少卖设计:扣减屏障机制
- 六、性能效果:TPS提升1倍以上
- 七、总结
一、前言:热点库存扣减的挑战
在电商系统中,库存扣减是一个非常核心且棘手的问题。尤其是遇到"热点商品"场景------比如直播间带货,数万用户同时抢购几百件商品,库存扣减的瞬时压力巨大。
传统的数据库扣减方案在这种场景下往往会成为瓶颈,只能通过限流来保护数据库。但限流意味着拒绝用户请求,直接影响业务营收。
最近读了一篇阿里云开发者社区的文章《库存合并扣减:一种基于分布式缓存的强一致性热点库存扣减方案》,分享了一套非常巧妙的解决方案:既利用了Redis的高性能,又保证了库存数据的强一致性,实现了零超卖、零少卖。
今天把这套方案的核心设计整理出来,希望能给遇到类似问题的同学一些启发。
二、问题分析:传统Redis分桶方案的三大缺陷
说到热点库存扣减优化,最常见的就是"Redis分桶扣减"方案:把库存从MySQL划分到多个Redis分桶,扣减时直接操作Redis。
但文章指出,这种传统方案在实际应用中存在三大缺陷:
缺陷一:无法避免库存少卖
Redis操作可能超时,应用层无法判断是成功还是失败:
- 扣减超时 → 默认当做失败(避免超卖)
- 库存回补超时 → 默认当做成功(避免少卖)
这导致库存"有损",只适用于卖家限购、权益等非实物类场景(业务接受少卖)。
缺陷二:无法支持复杂库存模型
传统Redis方案使用incr/decr实现库存加减,利用lowBound防止减到负数,value就是剩余库存数量。这种方案只支持单一数值的库存模式。
但实际的库存模型更复杂,比如:
- sq(可售库存):实际可售卖的库存
- wq(预扣库存):用户下单后,sq转向wq
- oq(占用库存):付款后,wq转向oq
对于sq-同时wq+这种复杂模式,传统Redis方案无法支持。
缺陷三:完全依赖Redis稳定性
如果Redis异常,整个扣减链路就会异常,系统容错性差。
传统Redis分桶方案
缺陷一: 少卖风险
缺陷二: 不支持复杂模型
缺陷三: 依赖Redis稳定性
超时场景无法判断
只支持单一数值
Redis异常=链路异常
三、核心思路:Redis做计数 + DB明细为准
那么,有没有一种方案既能利用Redis的高性能,又能保证强一致性呢?
文章给出的核心思路非常巧妙:Redis只用来做扣减计数以防止超卖,实际扣减是否成功以库存扣减明细为准。
整体设计原则
- DB行的库存提前预留(锁)一部分:锁的库存还在DB行上,同时也初始化到Redis里用于扣减计数
- 先扣Redis,再插DB明细:Redis库存够扣,再插入DB明细,明细插入成功才算扣减成功
- 合并提交:延迟扫描扣减明细,计算实际扣减数量,一次提交库存变更到DB
否
是
否
是
异步
用户下单
扣减Redis分桶
Redis库存够?
扣减失败
插入DB明细
明细插入成功?
扣减成功
合并提交模块
扫描扣减明细
计算实际扣减量
合并更新DB库存
核心概念:库存扣减明细(单据)
在介绍具体模块之前,需要理解"库存扣减明细"这个核心概念:
- 记录扣减信息:下单时交易告知扣减多少库存,后续付款或取消时,库存内部从明细恢复
- 幂等性保障:同一单据调用两次扣减、单据回补超时重试,都可以通过明细实现幂等
- 状态流转管理:负责库存扣减的生命周期状态流转
明细是不可或缺的,是整个方案"以DB为准"的基础。
四、方案详解:四大核心模块
基于上述核心思路,方案设计了四大核心模块。
模块一:锁库存模块(sq → lq)
核心动作 :将DB行库存从sq(可售)锁到lq(预锁库存),同时生成锁库存单据。
巧妙设计:
- 不是通过
sq - X + lq的方式,而是sq不动,直接加到lq - DB扣减时通过
WHERE sq - lq > 0控制最终sq不能小于lq
这个设计的优势:库存展示链路完全不受影响,查询可售库存时不需要关心锁的库存值,大大降低了链路复杂性。
原始库存
sq=100, lq=0
锁库存50
sq=100, lq=50
Redis初始化
分桶库存=50
每次锁定的lq数量,会初始化到Redis分桶中进行扣减计数。多个Redis分桶通过分桶缓存key进行索引。
模块二:下单扣减模块
流程:
- 先扣减Redis分桶库存
- 成功则继续插入DB明细
- 失败则兼容走老的DB扣减流程
否
是
否
是
下单请求
扣Redis分桶
扣减成功?
走DB扣减流程
插入扣减明细
插入成功?
返回扣减成功
模块三:合并提交模块
这是方案的"核心中的核心"。
流程:
- 失效Redis分桶:防止后续继续有交易流量扣减此分桶
- 扫描扣减明细:根据分桶key扫描所有关联明细
- 计算扣减数量 :通过
SUM(quantity)走覆盖索引得到实际扣减量 - 合并更新DB:一次性提交库存变更
合并提交触发
失效Redis分桶
扫描扣减明细
SUM计算实际扣减量
走覆盖索引
合并更新DB库存
性能关键 :明细表设计了覆盖索引(invId, lockOrderId, quantity),压测表明此SQL对DB热点扣减的性能影响非常小。
模块四:库存回收模块
库存回收指将预锁定的库存释放回可售卖状态。
需要回收的场景:
- 商家编辑场景:总库存编辑成0,需要先释放预锁库存
- 临界场景:Redis分桶只有1件,DB有2件,下单2件时需要先回收Redis库存再继续扣减
实现方式:直接调用分桶合并提交来完成。
五、防超卖少卖设计:扣减屏障机制
文章重点强调了防超卖少卖的设计,这部分非常关键。
DB层防超卖
通过SQL层面的条件控制:
sql
UPDATE inventory
SET sq = sq - oq
WHERE sq - lq - oq > 0
保证sq的数量不会小于lq,从数据库层面杜绝超卖。
Redis层防超卖
分桶的剩余库存值不能小于0,保证Redis层面也不超卖。
DB明细为准(防少卖)
如果Redis数据丢失了,实际扣减了多少库存,以DB合并下单明细为准,不会少卖。
扣减屏障机制(高并发防超卖)
高并发场景下,下单扣减和合并提交单据扫描并发执行时,可能会出现少扫描单据的情况,导致超卖。
文章设计了一个"扣减屏障"机制来防止这个问题:
是
否
下单扣减
检查扣减屏障
屏障存在?
扣减失败
执行Redis扣减
插入DB明细
合并提交
设置扣减屏障
扫描明细
合并扣减DB
删除扣减屏障
扣减屏障的作用:合并提交开始时设置屏障,阻止新的下单扣减;合并完成后删除屏障,恢复下单。这样可以保证扫描单据的完整性,避免并发导致超卖。
六、性能效果:TPS提升1倍以上
最后看看这套方案的实际效果。
压测效果
单行热点扣减场景下:
- 非合并扣减:TPS峰值约1.4万
- 合并扣减:TPS峰值3.2万
整体提升1倍以上的扣减性能。
线上效果
由于入口做了限流,线上达不到压测的流量峰值,但差异体现在成功率和耗时上:
| 指标 | 合并扣减 | 非合并扣减 |
|---|---|---|
| 扣减成功率 | 100% | 80%左右 |
| 接口耗时 | 7ms+ | 15ms+ |
单行热点扣减TPS对比 非合并扣减 合并扣减 4 3.5 3 2.5 2 1.5 1 0.5 0 TPS(万)
七、总结
读完这篇文章,我对这套库存合并扣减方案的核心思想有了清晰的理解:
- 设计原则:Redis做计数,DB明细为准 ------ 既利用了Redis的高性能,又保证了强一致性
- 巧妙设计:锁库存时sq不动直接加lq,展示链路完全不受影响
- 核心机制:合并提交 + 扣减屏障,解决了高并发场景下的超卖问题
- 实际效果:TPS提升1倍以上,成功率100%,耗时减半
这套方案给我的启发是:分布式系统的设计不一定要"非此即彼" ------ 可以把不同存储介质的优点结合起来,Redis负责高性能计数,DB负责数据一致性,通过明细机制实现两者的协同。
如果你的业务场景也面临热点库存扣减的挑战,希望这篇文章能给你一些参考。记住:方案的选择要结合业务特点,没有银弹,但有最合适的解法。