库存扣减如何避免超卖和少卖?
1. 问题根源分析
- 超卖原因:
- 并发竞争:多个用户同时请求同一商品库存,未加锁或原子性操作导致库存扣减覆盖(如库存100,两个请求同时读100,各自扣减后写回99,实际应扣减到98)。
- 缓存与数据库不一致:缓存(如Redis)中库存未及时同步到数据库,或数据库事务未提交时已更新缓存。
- 少卖原因:
- 事务回滚未恢复库存:扣减库存后因订单创建失败、支付超时等场景回滚事务,但未恢复库存。
- 异步处理丢失:消息队列消费失败未重试,导致已扣减的库存未真正占用。
2. 核心解决方案
2.1 数据库层保障
- 悲观锁(行锁):
- 在事务中通过
SELECT FOR UPDATE
锁定库存行,确保串行操作。
sqlBEGIN; SELECT stock FROM item WHERE id = 123 FOR UPDATE; -- 锁定行 UPDATE item SET stock = stock - 1 WHERE id = 123 AND stock >= 1; COMMIT;
- 在事务中通过
- 乐观锁(CAS机制):
- 基于版本号或库存值进行条件更新,避免直接覆盖。
sqlUPDATE item SET stock = stock - 1, version = version + 1 WHERE id = 123 AND stock >= 1 AND version = {current_version}; -- 返回影响行数,若为0则说明库存不足或版本冲突,需重试或拒绝
2.2 缓存层优化
- 预扣库存(Redis原子操作):
- 使用Redis的
DECR
或INCRBY
原子指令扣减库存,避免并发覆盖。
redisSET item:123:stock 100 -- 初始化库存 DECR item:123:stock -- 原子扣减,返回当前值
- 防超卖逻辑:扣减后检查结果值是否≥0,若为负数则回滚(
INCR
恢复)。
- 使用Redis的
- 缓存与数据库一致性:
- 采用双写策略(先更新数据库再删缓存)或订阅数据库Binlog同步缓存。
2.3 异步队列削峰
- 请求串行化:
- 将库存扣减请求发送到消息队列(如Kafka),由消费者单线程处理,确保顺序性。
java// 生产者发送消息 kafkaTemplate.send("stock-topic", itemId, userId); // 消费者单线程消费 @KafkaListener(topics = "stock-topic") public void consume(String itemId, String userId) { deductStock(itemId); // 串行扣减 }
- 预占库存与超时释放:
- 用户下单时预占库存(状态为“预占”),支付成功后转为“已扣减”,超时未支付则释放。
sqlUPDATE item SET prehold_stock = prehold_stock + 1, available_stock = available_stock - 1 WHERE id = 123 AND available_stock >= 1;
2.4 分布式锁控制
- Redis分布式锁:
- 使用Redisson或Lua脚本实现锁机制,确保同一商品库存扣减的原子性。
lua-- Lua脚本(原子执行) if redis.call('GET', KEYS[1]) >= ARGV[1] then return redis.call('DECRBY', KEYS[1], ARGV[1]) else return -1 end
3. 异常场景兜底
- 少卖补偿:
- 定时对账任务:每小时扫描“预占库存但未支付”的订单,自动释放库存。
- 事务回滚监听:通过数据库事务钩子(如Spring
@TransactionalEventListener
),在回滚时恢复库存。
- 超卖熔断:
- 实时监控库存阈值,当剩余库存≤5%时触发限流(如Sentinel),防止雪崩。
4. 业务规则增强
- 分层库存设计:
- 区分可用库存(前端展示)、预占库存(下单未支付)、实际库存(仓库物理库存)。
sqlCREATE TABLE item ( id INT PRIMARY KEY, total_stock INT, -- 总库存 available_stock INT, -- 可售库存 prehold_stock INT -- 预占库存 );
- 动态库存调整:
- 根据销售速率动态调整前端显示的库存量(如每秒减少N个),避免瞬时超卖。
5. 面试回答示例
问题:“如何避免库存扣减时的超卖和少卖?”
回答结构:
- 问题分析:说明并发竞争、数据不一致等核心原因。
- 分层解决方案:
- 数据库层:悲观锁(行锁)与乐观锁(CAS)的实现;
- 缓存层:Redis原子操作预扣库存;
- 异步队列:请求串行化与预占超时释放;
- 分布式锁:Redis Lua脚本保证原子性。
- 兜底机制:定时对账、事务回滚监听、熔断限流。
- 案例补充:举例说明某秒杀系统通过Redis预扣库存+数据库最终一致,支撑10万QPS且零超卖。
- 扩展思考:提及分库分表策略(库存按商品ID散列)以应对高并发场景。
6. 总结
通过 数据库锁机制、缓存原子操作、异步削峰、分布式锁 四层防护,结合 兜底对账 与 动态规则,可系统性解决超卖与少卖问题。需根据业务场景权衡方案(如秒杀场景优先性能,普通订单优先强一致性)。