库存系统设计
问题
如何设计一个防超卖的库存系统?
答案
库存扣减三层架构
Redis 预扣库存(Lua 原子操作)
Redis Lua 保证原子性
String lua = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock < tonumber(ARGV[1]) then
return -1
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])
""";
public boolean deductStock(String skuId, int quantity) {
Long remaining = redisTemplate.execute(
new DefaultRedisScript<>(lua, Long.class),
List.of("stock:" + skuId),
String.valueOf(quantity)
);
return remaining != null && remaining >= 0;
}
数据库扣减(乐观锁兜底)
乐观锁防超卖
UPDATE inventory
SET stock = stock - #{quantity}, version = version + 1
WHERE sku_id = #{skuId} AND stock >= #{quantity} AND version = #{version}
库存回补
订单取消/支付超时回补库存
public void rollbackStock(String skuId, int quantity) {
// 1. 回补 Redis
redisTemplate.opsForValue().increment("stock:" + skuId, quantity);
// 2. 回补数据库
inventoryMapper.increaseStock(skuId, quantity);
}
库存预热
大促前将库存加载到 Redis
public void warmUpStock(List<String> skuIds) {
for (String skuId : skuIds) {
int stock = inventoryMapper.getStock(skuId);
redisTemplate.opsForValue().set("stock:" + skuId, String.valueOf(stock));
}
}
常见面试问题
Q1: Redis 和数据库库存不一致怎么办?
答案:
- 定时对账:比较 Redis 和 DB 的库存差异
- Redis 作为预扣快速判断,DB 是最终真实数据
- MQ 消费失败时有重试机制 + 对账补偿
Q2: 分布式场景下如何防超卖?
答案:
三道防线:
- Redis Lua 原子扣减(最快,过滤 99% 请求)
- MQ 串行化消费(避免并发)
- DB 乐观锁/悲观锁(最终兜底)
Q3: 预扣后未支付怎么办?
答案:
设置订单超时(如 30 分钟),超时未支付通过延迟消息触发库存回补。详见 订单系统设计。