本文目录导读:

- 方案一:数据库直接扣减(乐观锁/悲观锁)
- 方案二:Redis 缓存扣减 + 异步同步数据库
- 方案三:预扣库存(Redis + 数据库事务补偿)
- 方案四:最终一致性方案(消息队列)
- 方案五:流量漏斗式分层设计(大型项目标配)
- 总结对比表
- 避坑建议
库存扣减是电商、票务等系统中最核心也最棘手的问题之一,设计不当会导致超卖(用户体验差)或少卖(库存浪费)。
以下整理了几种主流方案,按性能从低到高、复杂度从低到高排序,并分析了各自的优缺点和适用场景。
数据库直接扣减(乐观锁/悲观锁)
这是最基础、最直接的方案,直接在关系型数据库层面保证原子性。
乐观锁(最常见)
利用数据库的UPDATE ... WHERE stock > 0来保证不超卖。
- SQL示例:
UPDATE inventory SET stock = stock - 1 WHERE sku_id = ? AND stock > 0; - 原理: 数据库的行锁机制保证了这条SQL的原子性,如果
stock数量不足,更新影响行数为0,代码返回扣减失败。 - 优点: 实现简单,不需要额外组件,数据绝对准确。
- 缺点: 性能瓶颈在数据库,高并发下,大量请求会争抢行锁,导致TPS下降,数据库连接池打满后,系统可能雪崩。
悲观锁(SELECT ... FOR UPDATE)
- 原理: 查询库存时直接加锁,其他查询必须等待。
- 优缺点: 比乐观锁更简单,但性能极差,基本不用于高并发场景,容易造成死锁。
适用于低并发(如后台管理系统、企业ERP)或对一致性要求极高且流量不大的场景。
Redis 缓存扣减 + 异步同步数据库
这是目前互联网公司处理高并发秒杀、抢购的主流方案,核心思路是:把扣库存的操作从数据库搬到内存(Redis)中执行。
- 核心命令:
DECR stock(自减1) 或EVAL执行 Lua 脚本。 - 流程:
- 启动时,将数据库的库存加载到 Redis(
SET sku:1001:stock 10)。 - 用户请求到来,通过 Redis 的原子命令扣减库存。
- 扣减成功(返回大于等于0),则继续处理后续逻辑(如创建订单);扣减失败,则返回“已售罄”。
- 异步线程/队列,将 Redis 中的成功扣减记录逐步同步回数据库。
- 启动时,将数据库的库存加载到 Redis(
为什么用 Lua 脚本?
避免“读库存 -> 判断 -> 扣减”这个三步操作因为高并发出现并发问题,利用 Redis 单线程特性,将三步封装在一个 Lua 脚本里执行,保证原子性。
- 优点: 性能极高(单机QPS可达10万+),能抗住瞬时峰值流量。
- 缺点:
- 数据弱一致: Redis 是内存数据库,可能宕机丢数据。
- 复杂问题: 需要处理缓存与数据库的一致性,可能需要回滚(Redis 扣了,但后续用户取消订单,需要加回 Redis 库存)。
- 操作复杂性: 需要额外维护 Redis 集群。
适用于高并发、但允许极小概率数据不一致(可在后续补偿)的场景。
预扣库存(Redis + 数据库事务补偿)
这算是对方案二的增强,用于处理“用户下单后不支付占着库存”的问题。
-
逻辑:
- 用户下单时,在 Redis 中执行预扣(如
DECR)。 - 预扣成功,生成一个预占库存的订单(状态为“待支付”),将信息写入 MQ 或异步落库。
- 如果用户支付成功: 数据库正式扣减,订单状态改为“已支付”。
- 如果用户未支付/超时取消: 系统需要回补库存,即把 Redis 的库存加回来,并修改数据库中的订单状态为“已取消”。
- 用户下单时,在 Redis 中执行预扣(如
-
优点: 解决了“恶意下单刷库存”的问题,更接近真实业务逻辑。
-
缺点: 引入了“回补”逻辑,如果回补失败,库存就会“丢失”,需要额外的定时任务或延迟队列来处理超时订单。
适用于电商平台、演唱会购票等需要有支付环节且需防止黄牛占用库存的场景。
最终一致性方案(消息队列)
该方案的核心思想是 “请求入队,异步串行化处理”。
-
流程:
- 用户发起扣库存请求,系统不直接操作数据库,而是将请求(包含用户ID、SKU ID、数量)写入一个消息队列(如 Kafka、RocketMQ)。
- 一个独立的库存消费者单线程(或分区有序)消费这个队列中的消息。
- 消费者收到消息后,执行数据库的库存扣减。
- 扣减成功,通知用户;扣减失败,则需要进行重试或回滚上游。
-
优点:
- 削峰填谷: 能应对极高的请求峰值,系统压力平稳。
- 数据最终一致: 只要消息不被丢失,最终数据一定是准确的。
-
缺点:
- 延迟: 用户无法立刻知道是否扣减成功(通常需要等待几秒到几十秒)。
- 系统复杂: 需要引入消息队列中间件,处理消息重复、丢失、补偿等问题。
适用于对实时性要求不高,但必须保证数据最终不超卖的场景(如部分后台订单处理、库存调拨)。
流量漏斗式分层设计(大型项目标配)
现实中高并发系统通常不会只用一种方案,而是组合上述策略,形成“漏斗”结构。
用户请求
|
v
【第一层:本地内存限流/布隆过滤器】
| (挡住无效请求,如请求一个不存在的商品ID)
v
【第二层:Redis 原子扣减(高速屏障)】
| (挡住大部分超出库存的请求)
v
【第三层:消息队列/异步写库】
| (将成功的请求串行化,落库持久化)
v
【第四层:数据库最终一致性写入】
| (用乐观锁 `UPDATE stock > 0` 做兜底保证)
核心原则:
- 动静分离: 商品详情页、库存静态化。
- 流量管控: 在前端/网关层就拦截无效请求(同一个用户1秒内多次点击,只放行第一次)。
- 缓存先行: 99%的流量在 Redis 层就被处理掉或拒绝了。
- 数据库兜底: 数据库的乐观锁作为最后一道防线,保证绝对不会超卖。
总结对比表
| 方案 | 核心组件 | 性能 | 一致性 | 复杂度 | 典型场景 |
|---|---|---|---|---|---|
| 数据库乐观锁 | MySQL | 低 | 强一致性 | 低 | 后台管理、低并发业务 |
| Redis原子扣减 | Redis + DB | 高 | 最终一致性 | 中 | 秒杀、抢购、抽奖 |
| 预扣+补偿 | Redis + MQ + DB | 高 | 最终一致性 | 高 | 电商下单、票务(需支付) |
| 消息队列 | Kafka/RocketMQ + DB | 中 | 最终一致性 | 中 | 异步库存同步、跨系统 |
| 分层漏斗 | L1:限流 L2:Redis L3:MQ L4:DB | 极高 | 最终一致性 | 极高 | 双11、高并发大型系统 |
避坑建议
- 不要只用数据库做高并发扣减。 数据库的锁和磁盘IO扛不住。
- Redis 方案必须考虑数据丢失。 要做好库存的定期持久化(RDB/AOF),并建立库存校正机制(比如每日凌晨比对 Redis 和 DB 的库存差)。
- 任何方案都要有“反作弊”设计。 比如限制单用户购买数量、风控拦截等。
- 扣减和支付要联动。 不要下单就扣死库存,要设计“超时释放”或“支付才真正扣除”的策略。
综合来看,对于大多数业务,方案二(Redis扣减 + 异步同步数据库) 是性价比最高的选择,如果业务非常复杂且需要支付环节,可以考虑方案三。