防超卖的库存一致性要靠数据库锁、缓存原子操作和异步对账三层技术协同兜底,单一技术方案在极端并发下都撑不住,组合拳才能既保性能又保准确。
库存怎么被“卖超”的:一次典型的并发事故
先看一个真实的高并发场景,秒杀开始时,某SKU剩最后1件库存,两个用户的订单请求同时到达,如果没有兜底措施,两个请求都会读到“库存还有1件”,各自完成下单扣减,最终库存变成-1,超卖就发生了。
问题根源在于:多个请求读到了同一份库存快照,并且都认为自己有权扣减,常规的编程逻辑是“先读库存,再判断是否大于0,然后执行减少”,这中间存在时间差,并发请求一旦穿透这个时间差,就会重复扣减。
从技术层面看,这里涉及两个基础概念:
- 事务隔离级别,默认的REPEATABLE READ允许并发读,判断库存时多条事务会看到相同的数据版本。
- 竞态条件,两个线程同时执行“读取-判断-写入”三步操作,没有加锁或原子操作保护。
不少团队最初靠“代码加锁”解决问题,在Service层用synchronized或ReentrantLock锁住扣减方法,这种方案在单机部署时有效,但一旦应用扩容到多节点,本地锁各管各的,锁不住另一个JVM的请求,超卖又重新出现,这就是为什么需要一套分层的库存一致性方案。
数据库层兜底:库存扣减的最后一公里
数据库是最可靠的库存一致性兜底层,因为它天然支持事务和行锁,行业共识认为,任何缓存方案都不能完全替代数据库的最终校验。
用条件更新语句防超卖
最直接的招数是把“读-判断-写”变成一条原子SQL:
UPDATE inventory SET stock = stock - 1 WHERE sku_id = 123 AND stock > 0;
执行这条语句后,影响行数为1说明扣减成功,为0说明库存不足,数据库的行锁和条件判断在同一事务中完成,不会出现并发读到相同库存的情况,这条SQL本身就能挡住超卖,性能不如缓存,适合作为最终防线。
悲观锁兜住修改前的那一瞬
另一种做法是查询库存时用SELECT ... FOR UPDATE,对库存行加上排他锁,事务A锁住库存行后,事务B必须等待A提交才能继续操作,这种方案安全性高,但锁等待会拉低吞吐量,在高并发秒杀场景下数据库容易成为瓶颈。
乐观锁靠版本号减少锁竞争
给库存表加一个version字段,扣减时带上版本条件:
UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE sku_id = 123 AND stock > 0 AND version = 7;
如果版本号不匹配,说明库存被其他事务改过,需要重新读取重试,这个方案避免了长时间锁行,但大量并发失败重试会放大数据库压力。
唯一索引做“不可能超卖”的兜底
为了防止重复扣减,可以在订单表上建立订单号+SKU的唯一索引,即使上层逻辑出了bug,同一个用户对同一个SKU的重复下单也会触发唯一约束错误,这是物理层面的最后一道防线,不依赖代码逻辑。
数据库层兜底的核心原则是:缓存可以弹回来,数据库必须守恒,即使Redis被击穿、锁过期,只要数据库扣减语句正确,超卖就不会成真。
缓存层兜底:Redis原子扣减扛住高并发
纯数据库方案在高并发下性能不够,多数电商平台把库存先放Redis,用原子操作挡住第一波流量。
Redis的DECR天然防并发
Redis的DECR key是原子操作,多个客户端同时对同一个key执行DECR,结果一定符合预期,扣减库存的代码逻辑变为:
- 调用
DECR sku_stock_123拿到扣减后的剩余值。 - 如果剩余值小于0,说明卖超了,立即执行
INCR回补,并返回售罄。 - 如果剩余值大于等于0,继续走下单流程。
这个方案比数据库条件更新快一个数量级,因为Redis单线程模型天然串行化并发操作。
Lua脚本让判断和扣减不再分离
刚才的“DECR后再判断是否小于0”存在一个极小的时间差,如果进程在DECR之后崩溃,库存就永久少了一件,更稳妥的写法是用Lua脚本把判断和扣减合并为一步:
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) <= 0 then
return 0
end
redis.call('DECR', KEYS[1])
return 1
Redis执行Lua脚本是原子的,脚本执行期间不会插入其他命令,这段脚本保证“库存足够才扣减”,扣减失败不会产生负库存。
缓存与数据库的差异怎么对齐
Redis扣减成功并不代表数据库同步成功,你需要走一步异步回写,让Redis的扣减结果最终落到数据库,常见做法是:
- 下单成功后向MQ发送“已扣减库存”消息。
- 消费者任务执行数据库条件更新,更新失败则记录异常。
- 定时对账任务扫描Redis与数据库的库存差异,差额做补偿处理。
这套机制下,高并发流量先被Redis吸收,数据库异步消峰,两边数据最终一致,代价是可能出现“Redis扣了但数据库还没扣”的短暂窗口,但对账任务会在秒级内修复差异。
分布式锁兜底:别让多节点同时抢一件库存
当应用部署了多个节点,本地锁失效,Redis原子操作又被当作唯一依赖时,还有一个隐患:同一个订单的重试请求、多个微服务同时操作库存,这时候需要一把全局锁,让大家排队干活。
基于Redis的分布式锁
用SETNX命令加锁:
SET lock_sku_123 request_id EX 5 NX
只有抢到锁的节点才能执行扣减逻辑,处理完释放锁,这里有个关键细节:锁一定要设置过期时间,否则持有锁的节点宕机,其他节点永远等不到锁,业内专家指出,分布式锁的过期时间合理值通常在200-500毫秒,具体要看业务中扣减库存的耗时,设太短容易误删锁,设太长故障恢复慢。
减少锁粒度才能保住吞吐量
避免把“全局锁”做成一个进程锁住所有SKU的库存,正确做法是按SKU维度加锁,lock_sku_123只锁商品123的库存,不同商品互不干扰,更进一步,可以对一个SKU做库存分片,比如把1000件库存拆成10个key,每个key存100件,扣减时随机选一个分片操作,分片后并发能力提升,但对账复杂度也加大。
分布式锁的三难权衡
分布式锁确实能兜底,但它不是银弹:
- 加锁会限制Redis的并发能力,每秒几万的流量会让锁等待耗时飙升。
- 锁过期时间短,长事务还没执行完锁就释放了,第二个节点又进来重复扣减。
- 锁过期时间设置过长,节点宕机后锁的TTL期间内大量请求被阻塞。
所以分布式锁适合用在对一致性要求高但并发量可控的场景,比如库存预热、后台调拨、异常订单手工处理,秒杀主链路更适合依赖Redis原子操作。
对账与补偿:库存一致性方案的收尾手艺
前面几层技术都在尽力防止超卖发生,但现实世界不会总按写好的剧本走,缓存可能被穿透,数据库连接可能超时,MQ消息可能丢失,最后的兜底是一套对账补偿机制。
订单超时关单自动回补库存
用户下单后不一定支付,你需要设置一个订单超时时间,比如15分钟或30分钟,订单未支付被系统关闭时,同时执行库存回补:
- 修改订单状态为已关闭。
- 调用库存服务执行
INCR回补。 - 发送消息通知对账任务刷新库存快照。
定时对账扫描库存差异
开发一个有BUG出现不
写一个定时任务,每隔一定周期做一次库存校准:
- 读取Redis中的当前库存值。
- 读取数据库中扣减流水表和剩余库存。
- 计算订单扣减总量,对比Redis库存差异。
- 发现差异时,以数据库流水为准修正Redis库存,并记录异常单。
这个过程能兜住“Redis库存回补失败”“数据库扣减重复”“消息丢失”三类常见问题,多数情况下,对账任务发现差异后能自动修复,少数无法自动修复的case才需要人工介入。
技术组合怎么选:一套能落地的库存一致性方案
不同阶段和不同体量的业务,技术搭配差异很大,先用一张表做对比,比如一个中小型电商项目:
| 方案 | 防超卖能力 | 并发性能 | 落地成本 | 适用场景 |
|---|---|---|---|---|
| 纯数据库条件更新 | 强 | 低 | 低 | 库存量小、并发低的后台系统 |
| Redis原子扣减+Lua | 较强 | 高 | 中 | 秒杀、抢购第一期流量冲击 |
| 数据库+Redis异步对账 | 强 | 高 | 高 | 主流电商主链路 |
| 分布式锁兜底 | 中 | 中 | 中 | 多节点任务调度、后台补货 |
高并发下单库存怎么扣减的推荐组合
如果要从零搭建一套生产可用的方案,推荐主链路这么设计:
- 流量入口用Redis Lua脚本扣减库存,扛住峰值并发。
- 扣减成功后异步写MQ消息,消费者更新数据库库存。
- 数据库层用
UPDATE ... WHERE stock > 0做最终校验,失败则回滚Redis并关单。 - 定时对账每5分钟跑一次,修复史诗级差异。
- 压测验证用JMeter模拟超过预期峰值3倍的并发请求,观察是否存在超卖。
一套靠谱的库存一致性方案有哪些硬指标
好的方案不只看能不能防超卖,还要看几个指标:
- 扣减成功率,秒杀场景下大量请求会提示售罄,这本身合理,但成功扣减的订单库存不允许超出可用库存。
- 恢复时长,Redis和数据库库存不同步时,对账修复耗时要控制在秒级到分钟级。
- 故障降级路径,Redis宕机时库存服务能自动切到数据库条件更新,而不是直接返回失败。
- 可观测性,每个扣减请求有唯一traceId,从Redis到数据库的全链路可以追踪。
防超卖这件事,没有“一招鲜”的魔法,用数据库条件更新兜住守恒底线,用Redis原子操作扛住并发峰值,用分布式锁护住多节点协作,再用对账任务收拾残局,四者各司其职才能形成完整的库存一致性方案。每多一层兜底,超卖风险就指数级下降,但每多一层也会多一份延迟和复杂度,关键在于结合自己的并发体量找到平衡点。
防超卖怎么做?这些问题值得深挖
什么是库存一致性它和超卖有什么关系
库存一致性指的是Redis、数据库、订单系统中的同一SKU库存数值在任何时刻都不发生互相矛盾,超卖就是三个数据源出现差异时引发的订单数量超过库存数量的现象,确保库存一致性,本质上就是在并发写操作下维持“扣减总量≤初始库存”的不变量,并通过事务、锁、对账等手段让各个存储层收敛到同一个数值。
高并发下单库存怎么扣减才能不超卖
优先采用Redis原子扣减加Lua脚本,用整个脚本保证“检查库存-扣减库存”两步的原子性,扣减成功后异步通知数据库更新,数据库执行条件更新校验一次最终库存,在订单表上建唯一索引挡住重复下单,这套链路的核心思路是让Redis做快速吞吐,让数据库做终极仲裁。
缓存和数据库库存不一致时该信谁
以数据库的扣减流水和库存余额为准,Redis中的库存只是数据库库存的一层热数据缓存,它可能因为原子操作和异步消息的延迟而暂时偏离真实值,对账任务发现偏差后,应当基于数据库实际扣减流水修正Redis缓存,而不是反过来。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/774571.html


