很多开发者认为:只要 Redis 扣库存成功,并且 Redis 没有报错,就不会发生超卖。真实情况并非如此。本文通过一次电商秒杀系统的线上事故复盘,完整分析从事故发生、排查过程、错误推断、根因定位到最终修复的全过程,并总结高并发库存系统的工程实践。
.decrement("stock:1001");if(remain<0){thrownewRuntimeException("库存不足");}代码没有明显问题。
Redis DECR 是原子操作。
理论上不会超卖。
三、错误方向分析
错误方向1:Redis不原子
很多开发者会认为:
高并发下是不是 Redis 自己出了问题?
实际上:
Redis 单线程执行命令。
DECR 本身具有原子性。
所以:
Redis原子 ≠ 整个业务原子这是很多人的误区。
错误方向2:主从延迟
有人怀疑:
读到了从库旧数据。
检查后发现:
库存操作全部走主库。
没有读写分离。
排除。
四、真正的问题出现了
继续追踪链路。
发现订单系统引入了 MQ。
架构如下:
事故期间。
RabbitMQ 出现积压。
部分消息发送超时。
这里埋下了隐患。
五、根因分析
出现以下情况:
场景A:
Redis扣减成功 ↓ MQ发送失败 ↓ 订单创建失败库存减少。
订单没有。
此时系统进入不一致状态。
为了恢复。
运维执行库存补偿。
库存+1问题看似解决。
实际上部分消息后来恢复成功。
于是:
库存补偿 + 订单再次创建最终导致超卖。
六、为什么监控没有发现
技术监控只关注:
- Redis
- MySQL
- MQ
- CPU
- 内存
但缺少业务监控。
例如:
库存扣减成功数 订单创建成功数 库存回滚数这些指标没人看。
于是:
技术正常。
业务异常。
七、如何复现问题
测试代码:
decreaseStock();sendMessage();thrownewRuntimeException();结果:
Redis库存减少 订单失败 库存未恢复数据开始不一致。
当补偿逻辑介入时。
风险进一步扩大。
八、解决方案对比
方案一:分布式锁
RLocklock=redissonClient.getLock("stock");lock.lock();try{//扣库存}finally{lock.unlock();}优点:
简单。
缺点:
吞吐量下降。
方案二:Lua脚本
localstock=redis.call('get',KEYS[1])iftonumber(stock)<=0thenreturn-1endredis.call('decr',KEYS[1])return1优点:
高性能。
缺点:
无法解决链路一致性。
方案三:库存预占
流程:
预占库存 ↓ 创建订单 ↓ 支付成功 ↓ 正式扣减失败则释放。
这是很多大型电商的方案。
九、幂等设计
消费端必须保证:
if(orderExists(orderNo)){return;}否则:
消息重复投递。
订单重复创建。
库存再次异常。
十、库存流水设计
建立流水表:
CREATETABLEstock_flow(idBIGINT,order_noVARCHAR(64),product_idBIGINT,change_numINT,create_timeDATETIME);任何库存变化必须记录。
方便审计。
方便追溯。
方便补偿。
十一、最终架构
上线后经历多次大促。
未再出现超卖。
十二、事故复盘总结
这次事故最重要的结论:
很多开发者把:
Redis原子操作等同于:
系统不会超卖实际上:
Redis 只能保证命令原子。
不能保证整个业务链路一致。
真正需要关注的是:
- 幂等
- 补偿
- 流水
- MQ可靠性
- 业务监控
我的建议
如果你的系统:
- 日订单 < 1000
直接数据库事务即可。
如果:
- 秒杀
- 抢购
- 高并发活动
一定要提前设计:
- 库存预占
- 幂等控制
- MQ重试
- 库存流水
- 业务监控
否则迟早会踩坑。
结语
线上事故最可怕的不是报错。
而是:
系统看起来一切正常。
监控一切正常。
用户却已经受到影响。
Redis 很强。
但 Redis 不是银弹。
真正解决超卖问题的,从来不是一个命令,而是一套完整的工程体系。
如果你长期使用 Cursor、Claude Code、ChatGPT Plus、Gemini Advanced、Grok、Kiro 等工具,也可以顺手了解 gpt108.com。它主要解决相关 AI 工具的订阅需求。但对于后端开发者来说,工程设计能力永远比工具更重要。