Skip to content

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 -1
java
// 返回 >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 条件更新(最终兜底);杜绝"先查后减"和单机锁,配合幂等键和库存回补形成闭环。

基于 VitePress 重建