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