0
0
0

Redis 库存超卖问题与解决方案

2022-03-26
2026-10-26
文章摘要
|

一、什么是库存超卖?

库存超卖是指在高并发情况下,多个线程同时判断库存充足,导致实际销售数量超过可用库存。

例如:商品库存只有 1 件。

线程 A                     线程 B
   |                          |
读取库存:1               读取库存:1
   |                          |
判断库存充足             判断库存充足
   |                          |
创建订单成功             创建订单成功
   |                          |
   └──────────┬───────────────┘
              ↓
      库存1件,却卖出2件

需要注意:如果两个线程都读取库存 100,分别扣减后都写入 99,这属于丢失更新,也会导致库存数据错误,但不一定已经发生超卖。

核心原因:库存判断与扣减操作没有保证原子性。

二、解决方案

主要可以采用悲观锁、乐观锁或原子操作。

1. 悲观锁

悲观锁认为并发操作可能产生冲突,因此先加锁,再执行库存扣减。

常见实现:

  • Java:synchronized、ReentrantLock

  • MySQL:SELECT ... FOR UPDATE(事务中使用)

  • Redis:SET NX PX 实现分布式互斥锁

注意: Java 本地锁只能保护同一个 JVM 中的线程。如果 Spring Boot 部署多个 Docker 容器,本地锁无法实现跨实例互斥。

2. 乐观锁

乐观锁假设并发冲突较少,更新时再判断数据是否发生变化。

方式一:版本号机制

为商品库存增加 version 字段。

UPDATE goods
SET stock = stock - 1,
    version = version + 1
WHERE id = 1
  AND stock > 0
  AND version = 5;

原理:

  1. 多个线程读取相同的库存和版本号。

  2. 第一个线程更新成功,版本号加 1。

  3. 其他线程使用旧版本号更新时,影响行数为 0,更新失败。

  4. 更新失败后,可根据业务决定重试或返回失败。

方式二:库存条件更新

如果只是扣减库存,通常不需要额外增加版本号。

UPDATE goods
SET stock = stock - 1
WHERE id = 1
  AND stock > 0;

通过判断受影响行数确定是否成功:

  • 1:库存扣减成功。

  • 0:库存不足或商品不存在。

这种方式利用数据库单条 SQL 的原子性,防止库存被扣成负数。

推荐:普通 MySQL 库存扣减,优先使用条件更新。

三、Redis 如何防止超卖?

当库存保存在 Redis 中时,可以使用 Lua 脚本实现原子扣减。

local stock = tonumber(redis.call('GET', KEYS[1]))

if not stock or stock < 1 then
    return 0
end

redis.call('DECR', KEYS[1])
return 1

返回值:

  • 1:库存扣减成功。

  • 0:库存不足或不存在。

Redis 会原子执行整个 Lua 脚本,避免多个请求分别读取、判断和扣减造成并发问题。

注意:SET NX PX 属于互斥锁实现方式,不是乐观锁。

四、悲观锁与乐观锁的区别

对比

悲观锁

乐观锁

核心思想

先加锁,再操作

更新时检查冲突

并发处理

通过互斥控制访问

条件不满足则更新失败

性能特点

可能产生等待

冲突少时开销较低

适用场景

冲突频繁、需要串行处理

冲突较少、允许失败重试

注意:乐观锁不一定始终比悲观锁快,高并发下频繁重试同样会产生性能问题。

五、实际开发建议

普通库存扣减:

使用 MySQL 条件更新:

UPDATE goods
SET stock = stock - #{quantity}
WHERE id = #{id}
  AND stock >= #{quantity};

库存扣减和订单创建应放在同一个数据库事务中,保证数据一致性。

高并发秒杀:

可以采用:

请求进入
   ↓
Redis Lua 原子扣减库存
   ↓
扣减成功
   ↓
创建订单(可通过消息队列异步处理)
   ↓
数据库事务处理库存与订单
   ↓
失败时进行库存补偿

还需要处理重复下单、消息重复消费、数据库失败和 Redis 与数据库的数据一致性。

六、总结

库存超卖的根本原因是并发情况下库存校验与扣减缺乏原子性。

  • 悲观锁:通过互斥保证并发安全。

  • 乐观锁:通过版本号检查并发冲突。

  • MySQL 条件更新:利用单条 SQL 原子扣减库存。

  • Redis Lua:保证 Redis 内库存校验与扣减的原子性。

实际开发中,普通库存优先考虑 MySQL 条件更新 + 数据库事务;高并发秒杀可以考虑 Redis Lua + 异步订单处理 + 幂等与补偿机制。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论