P53 项目场景:高并发库存扣减 & 防超卖设计
面试题:秒杀/抢购场景下怎么扣库存?怎么保证不超卖?
1. 不超卖的三个层面
① 数据库层(最后防线)
条件更新,库存不够就不更新:
sql
UPDATE t_sku SET stock = stock - 1
WHERE sku_id = ? AND stock > 0;
-- 影响行数 = 1 说明扣成功;= 0 说明没库存配合乐观锁/版本号也可以,但条件更新最简单可靠。
② Redis 层(高性能主战场)
库存预存在 Redis,用 Lua 脚本原子扣减:
lua
if tonumber(redis.call('GET', KEYS[1])) > 0 then
return redis.call('DECR', KEYS[1])
end
return -1java
// 返回 >0 表示扣减成功,允许下单
Long remain = redisTemplate.execute(luaScript, Arrays.asList("stock:" + skuId));③ 业务层(防止重复扣)
- 用户维度防重:同一用户同一商品只放行一次(Redis SETNX 幂等键);
- 下单超时未支付要回补库存(Redis + DB 都要回补)。
2. 常见错误写法(面试要会指出)
java
// ❌ 先查再减:两个线程都查到 stock=1,都减,超卖
int stock = getStock(skuId);
if (stock > 0) {
updateStock(skuId, stock - 1);
}java
// ❌ synchronized 只锁本机,集群下失效
synchronized (this) { ... }3. 完整流程(生产推荐)
text
请求 → 限流/防刷 → Redis Lua 预扣库存(原子)
→ 扣成功:发 MQ 异步创建订单
→ 消费端:校验 + 订单落库(DB 条件更新兜底)
→ 超时未支付:回补库存4. 加分点
- 说清"Redis 扣减 ≠ 最终库存":Redis 是准入控制,数据库条件更新才是最终保证;
- 提到 Redis 扣减要记录扣减流水(方便对账/回补);
- 分布式事务不需要强一致:允许短暂超卖状态,最终一致即可(如"下单但 30 分钟未支付");
- 极端情况:库存回补要和支付/取消状态机联动,避免重复回补。
一句话总结
防超卖三层:Redis Lua 原子预扣(高并发准入)→ MQ 异步落单(削峰)→ 数据库 UPDATE ... WHERE stock > 0 条件更新(最终兜底);杜绝"先查后减"和单机锁,配合幂等键和库存回补形成闭环。