防止库存超卖的几种方案

1. 乐观锁(Optimistic Locking)

✅ 适用场景

  • 适用于 低并发库存更新不频繁的场景(如普通商品购买)。
  • 适合 读多写少 的场景,避免锁的开销。

⚙ 实现方式

  • 在数据库表中添加 version(版本号)或 stock 字段。
  • 读取库存时获取 version,更新时使用 WHERE version = ? 来确保库存未被修改。

📌 代码示例

1
2
3
4
5
6
7
8
// 1. 查询当前库存和版本号
Stock stock = stockMapper.getStockById(productId);

// 2. 使用乐观锁更新库存
int rows = stockMapper.updateStock(productId, stock.getVersion());
if (rows == 0) {
throw new RuntimeException("库存不足或被并发更新");
}

📌 SQL

1
2
3
UPDATE stock_table 
SET stock = stock - 1, version = version + 1
WHERE product_id = ? AND stock > 0 AND version = ?

❌ 可能遇到的问题

  • 高并发下可能频繁更新失败(因为多个线程修改相同数据,只有一个会成功)。
  • 需要应用层进行重试,增加请求开销。
  • 不适用于高并发抢购,会导致大量失败请求。

⚠️注意

高并发下可能出现超卖

在高并发环境中,多个实例多个线程同时查询库存并认为库存充足,但在执行UPDATE(扣减库存)之前,其他线程已经更新了库存。

例如,假设当前库存为10,两个用户 A 和 B 同时查询库存:

  1. 用户 A用户 B 都查询库存,发现库存为 10。
  2. 用户 A用户 B 都进入了扣减库存的流程,并准备执行 UPDATE 操作。
  3. 用户 A 执行 UPDATE 时,库存减少到 9,但 用户 B 并不知道库存已被减少,依然认为有 10 件库存可以扣减。
  4. 用户 B 也执行了 UPDATE,库存减少到 9,但此时实际上库存已经不足,因为用户 A 已经扣减了库存。

为什么会超卖?

这是因为:

  • 用户 A 和用户 B 并没有及时同步库存数据,它们并没有基于实时的库存状态进行操作,反而是各自独立查询并各自更新。
  • 虽然查询时发现库存充足,但在执行更新时,实际的库存已经被其他线程或实例修改过,导致库存不足的情况没有被检测到,最终多扣减了库存,造成了超卖。

为什么这个问题会出现在乐观锁场景下?

乐观锁是在修改数据时进行检查,它假设大部分情况下没有竞争。因此,它会在执行时使用版本号或时间戳来校验库存是否发生过变化。如果没有发生变化,才会进行库存的扣减操作。

但在高并发情况下,多个实例或线程的操作仍然可能存在时间差,在检查和更新之间发生竞争。此时即使使用乐观锁(如通过版本号校验库存),也无法有效避免并发情况下的冲突,因为版本号数据的更新时间存在延迟,不同线程可能在不同时间修改数据,导致并发冲突,最终导致库存超卖


2. 悲观锁(Pessimistic Locking)

✅ 适用场景

  • 适用于中等并发的场景,确保库存更新时的线程安全。
  • 适合 关键资源竞争激烈 的情况(如限量抢购)。

⚙ 实现方式

  • 采用 SELECT ... FOR UPDATE 加行锁,确保同一时刻只有一个线程可以修改库存。

📌 代码示例

1
2
3
4
5
6
7
8
9
10
@Transactional
public void deductStock(Long productId) {
// 查询库存并加行锁
Stock stock = stockMapper.getStockForUpdate(productId);
if (stock.getStock() <= 0) {
throw new RuntimeException("库存不足");
}
// 更新库存
stockMapper.updateStock(productId);
}

📌 SQL

1
2
SELECT stock FROM stock_table WHERE product_id = ? FOR UPDATE;
UPDATE stock_table SET stock = stock - 1 WHERE product_id = ?;

❌ 可能遇到的问题

  • 数据库并发性能低,锁竞争激烈时会导致线程阻塞,影响吞吐量。
  • 事务超时问题,如果锁未释放,可能导致数据库死锁。

⚠️注意

不推荐在分布式架构中使用!

  1. 分布式数据库无法保证全局悲观锁
    • 在分库分表场景下,MySQL 无法直接加全局锁,不同节点的事务隔离,导致锁机制难以同步
    • 示例:
      • A 服务器在数据库1锁住商品库存
      • B 服务器在数据库2也锁住相同商品(但库存数据不一致)
      • 结果:库存状态可能不一致,影响数据完整性。
  2. 数据库性能瓶颈
    • 悲观锁一般是 SELECT ... FOR UPDATE,在高并发下可能导致数据库出现大量锁等待,影响整体性能。
    • 多个服务器竞争锁,导致用户下单等待时间变长,甚至数据库死锁。
  3. 事务超时
    • 分布式架构中,单机事务较短,但分布式事务涉及多个节点,可能导致锁超时问题,影响系统稳定性。
  4. 导致分布式死锁
    • 不同应用节点可能在不同数据库实例或 Redis 实例上获取锁,导致分布式死锁问题,影响可用性。

3. 分布式锁(Redis + Redisson)

✅ 适用场景

  • 适用于分布式部署的情况,防止多个应用实例同时操作库存。
  • 适用于 高并发但不适合数据库锁的业务(如订单支付场景)。

⚙ 实现方式

  • 使用 Redis SETNX(SET if Not Exists) 机制来加锁。
  • 使用 Redisson 进行自动续期和锁释放,防止死锁。

📌 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void deductStock(Long productId) {
String lockKey = "lock:stock:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock(10, TimeUnit.SECONDS); // 获取锁
Stock stock = stockMapper.getStockById(productId);
if (stock.getStock() <= 0) {
throw new RuntimeException("库存不足");
}
stockMapper.updateStock(productId);
} finally {
lock.unlock(); // 释放锁
}
}

❌ 可能遇到的问题

  • 锁超时释放问题,如果业务逻辑执行过长,锁可能提前释放导致并发问题。
  • 网络分区问题,可能导致多个节点同时获取锁,发生库存超卖(需要用 Redisson 解决)。

4. Redis 预扣库存

✅ 适用场景

  • 适用于 秒杀、抢购等高并发场景,避免数据库压力。
  • 适用于 库存实时性要求不高的业务(如电商促销)。

⚙ 实现方式

  • 先在 Redis 里存储库存值,每次扣减时先修改 Redis,后异步同步到数据库。

📌 代码示例

1
2
3
4
5
6
7
String key = "stock:" + productId;
Long stock = redisTemplate.opsForValue().decrement(key);
if (stock < 0) {
throw new RuntimeException("库存不足");
}
// 异步更新数据库库存
stockService.updateStockAsync(productId);

❌ 可能遇到的问题

  • 缓存一致性问题,Redis 和数据库可能不同步(需使用 定时任务或消息队列 补偿)。
  • 数据库库存回滚问题,Redis 预扣成功但支付失败时需归还库存

5. 消息队列(MQ)异步扣减库存

✅ 适用场景

  • 适用于超高并发(如双十一秒杀)。
  • 适用于流量削峰,让请求先进入消息队列,异步消费。

⚙ 实现方式

  • 订单请求先进入 Kafka / RabbitMQ 队列,后台消费者顺序扣减库存

📌 代码示例

1
2
3
4
5
6
7
8
// 订单下单时,发送库存扣减消息
rabbitTemplate.convertAndSend("stockQueue", productId);

// 监听消息,异步扣减库存
@RabbitListener(queues = "stockQueue")
public void consumeStock(Long productId) {
stockService.deductStock(productId);
}

❌ 可能遇到的问题

  • 消息丢失问题,需要启用 消息持久化
  • 消息顺序问题,需要确保一个商品的扣减消息按照 FIFO 顺序处理

方案对比总结

方案 适用场景 并发性能 可靠性 实现难度
乐观锁 低并发
悲观锁 中等并发
分布式锁 分布式
Redis 预扣库存 高并发
消息队列 极高并发 最高

场景举例

以下单场景,不同实现方案可能出现的问题:

方案 用户体验 可能遇到的问题
乐观锁 体验一般 订单可能失败,用户需多次尝试
悲观锁 体验较差 下单卡顿,可能等待较长时间
分布式锁 体验较好 短时间超卖可能性,锁竞争可能稍慢
Redis 预扣库存 体验较好 支付后可能发现库存不足(回滚问题)
MQ 异步扣减库存 体验最好 订单处理稍有延迟,可能排队失败

结论

  • 如果并发量较低,推荐乐观锁(简单高效)。
  • 如果有分布式架构,推荐Redis 分布式锁
  • 如果是高并发秒杀,推荐Redis 预扣库存 + 消息队列,提高性能。

你的业务并发量是多少?我们可以进一步优化 😊