库存系统如何避免超卖和少卖?
一、核心问题分析
问题类型 | 原因 | 后果 |
---|---|---|
超卖 | 并发请求下多个线程同时判定库存充足,导致扣减总和超过实际库存 | 订单无法履约,用户投诉 |
少卖 | 已扣减库存未释放(如订单未支付、系统异常回滚失败) | 商品滞销,营收损失 |
二、技术方案设计
1. 数据库层:强一致性控制
- 方案一:乐观锁(版本号控制)
UPDATE sku_stock
SET stock = stock - #{num},
version = version + 1
WHERE sku_id = #{skuId}
AND version = #{oldVersion}
AND stock >= #{num};
优点:无锁竞争,适合中等并发场景。
缺点:高并发下大量请求失败需重试。
- 方案二:悲观锁(SELECT … FOR UPDATE)
BEGIN;
SELECT stock FROM sku_stock WHERE sku_id = #{skuId} FOR UPDATE;
-- 业务逻辑校验
UPDATE sku_stock SET stock = stock - #{num} WHERE sku_id = #{skuId};
COMMIT;
优点:强一致性保证。
缺点:并发性能差,可能引发死锁。
- 方案三:直接库存约束
UPDATE sku_stock
SET stock = stock - #{num}
WHERE sku_id = #{skuId}
AND stock >= #{num}; -- 核心约束条件
优点:简单高效,依赖数据库原子性。
缺点:需处理更新结果为0的失败情况。
2. 缓存层:高性能扣减
- Redis+Lua原子操作
-- KEYS[1]:库存key, ARGV[1]:扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1 -- 成功
else
return 0 -- 失败
end
优点:支持万级TPS,避免数据库压力。
补充:需异步同步Redis与数据库数据。
3. 业务层:预占库存机制
- 流程:
+---------------------+
| 用户下单 |
+----------+----------+
| 预占库存
+----------v----------+
| 生成预占订单(待支付) |
+----------+----------+
| 支付超时/失败
+----------v----------+ +------------------+
| 定时任务释放预占库存 +-------> 恢复Redis+DB库存 |
+---------------------+ +------------------+
- 关键点:
- 预占库存有效期(如30分钟)。
- 支付成功后执行实际库存扣减。
- 使用延迟队列(RocketMQ延迟消息)触发释放。
4. 分布式环境下的一致性保障
- 最终一致性方案
1. 扣减Redis库存成功
2. 发送MQ消息(保证本地事务)
3. 消费者更新数据库库存
4. 定时对账修复差异
- 强一致性方案(TCC模式)
Try阶段:
- 冻结库存(stock_frozen字段+1)
Confirm阶段:
- 真实扣减库存(stock -= num, stock_frozen -= num)
Cancel阶段:
- 释放冻结库存(stock_frozen -= num)
5. 少卖问题的专项处理
- 库存释放策略:
- 未支付订单:定时任务扫描超时订单,调用库存回补接口。
- 订单取消:用户主动取消时立即触发库存恢复。
- 补偿机制(幂等库存回补接口)
@Transactional
public void restoreStock(Long skuId, Integer num) {
skuStockDao.updateStock(skuId, num); // 累加库存
orderDao.markStockRestored(orderId); // 标记避免重复回补
}
三、高并发优化策略
- 热点库存分片
// 对skuId取模分片到不同Redis节点
int shard = skuId % 16;
String redisKey = "stock:shard_" + shard + ":" + skuId;
- 本地缓存+批量合并
服务内存维护ConcurrentHashMap:
- Key: skuId
- Value: 待扣减数量(累计合并请求)
定时每100ms批量提交到Redis
- 令牌桶限流
// 每个SKU分配独立令牌桶
RateLimiter rateLimiter = RateLimiter.create(1000); // 每秒1000次
if (rateLimiter.tryAcquire()) {
// 允许执行库存操作
}
四、容灾与监控
- 库存对账系统
每日凌晨对比:
Redis库存总量 + 预占库存 = 数据库库存总量
若不相等,触发告警并自动修复
- 监控指标
- 实时看板:库存变更QPS、预占库存释放率、库存对账差异数。
- 报警规则:库存扣减失败率 > 1%、库存回补延迟 > 5分钟。
- 熔断降级
当数据库响应时间 > 500ms时:
1. 切换库存计算到Redis-only模式
2. 记录日志后异步补偿
五、方案选型建议
场景 | 推荐方案 | 优点 |
---|---|---|
普通电商(千级TPS) | 数据库乐观锁 + 预占机制 | 实现简单,数据强一致 |
秒杀系统(万级TPS) | Redis分片 + 异步对账 | 高性能,可扩展 |
分布式复杂业务 | TCC模式 + 本地缓存合并 | 高一致,支持柔性事务 |
核心总结
- 超卖防护:通过数据库/缓存原子操作、预占库存机制确保扣减不超量。
- 少卖解决:完善库存释放策略(定时任务、异步补偿)和幂等回补接口。
- 高性能:缓存分片、批量合并请求、令牌桶限流应对高并发。
- 高可靠:库存对账系统兜底数据一致性,熔断降级保障系统可用性。