如何解决高并发下的库存抢购超卖少买问题?

一、问题本质

“超卖”和“少买”是并发修改库存导致的数据不一致问题。

  • 超卖:库存 1 件,却卖出 2 件(库存减到负数)
  • 少买:库存足够,但有线程被锁或排队失败,导致明明能买却没买成功。

根因是:多个线程几乎同时读取相同库存值,然后同时修改,导致覆盖更新。
比如:

库存=1
线程A查库存=1 → 下单 → 库存=0
线程B查库存=1 → 下单 → 库存=-1(覆盖更新)

二、常见解决思路总览

我们可以从三个层面看:

层面 核心手段 举例
数据库层面 行锁、悲观锁、乐观锁 for update、version 字段
缓存/队列层面 异步削峰、单线程顺序消费 Redis + MQ
分布式协调层面 分布式锁、防重令牌 Redis、Zookeeper

三、具体实现方案

1. 悲观锁(数据库行锁)

做法:

select stock from product where id = 1 for update;
-- 判断库存>0
update product set stock = stock - 1 where id = 1;

这种方式在 MySQL InnoDB 下会锁住那一行,直到事务提交。

优点:
简单直观,适合低并发场景。

缺点:
锁粒度大,性能差;在高并发下大量阻塞或死锁。

适用:
中小系统或后台管理类接口。


2. 乐观锁(version 机制)

原理:
给库存表加 version 字段,每次修改时判断版本号是否一致:

update product set stock = stock - 1, version = version + 1
where id = 1 and stock > 0 and version = #{version}

若返回行数为 1 → 成功,否则重试。

优点:
无锁操作,高并发性能好。

缺点:
需要重试逻辑,可能失败率高(但可通过限流、MQ 缓解)。

实战建议:
结合 Redis 限流或消息队列使用。


3. Redis + Lua 脚本(缓存库存扣减)

核心思路:
库存不查数据库,直接在 Redis 中操作:

-- lua原子操作示例
local stock = tonumber(redis.call('get', KEYS[1]))
if stock > 0 then
redis.call('decr', KEYS[1])
return 1
else
return 0
end

优点:
原子性强(Lua脚本在 Redis 单线程中执行),速度极快。

缺点:
需要定时同步库存回数据库(防止 Redis 宕机丢数据)。

实战优化:

  • Redis 预减库存 + MQ 异步扣减数据库。
  • 超时未支付的订单库存回滚。

4. 消息队列(MQ)削峰 + 异步扣减

架构思路:
用户请求 → 放入 MQ 队列 → 消费者顺序处理下单逻辑(扣库存、生成订单)

好处:
削峰填谷,系统不直接承压。

关键点:

  • 队列消费要幂等(防止重复消费)
  • 消费失败要有重试机制
  • 可结合 Redis 预减库存(先扣库存,再入队)

典型组合:
Redis 预减库存 + RabbitMQ 异步落单。


5. 分布式锁(Redis / Zookeeper)

实现例:
使用 Redis SETNX 加锁:

String key = "lock:product:" + productId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);

拿到锁才能扣减库存。

问题:

  • 性能不如 Redis Lua 方案
  • 锁粒度设计要合理(商品级)

可优化:
Redisson 提供公平锁、看门狗机制,较稳定。


四、综合实战方案(推荐)

对于电商秒杀/抢购项目,通常采用:

1. Redis预扣减 + 消息队列异步落单

  • 用户请求到达 → Redis库存-1(原子Lua脚本)
  • 成功则入 MQ → 异步创建订单(数据库真正扣库存)
  • 支付超时回滚库存(补偿机制)

2. 加上限流、风控

  • 接口层:限流(如Guava RateLimiter、Nginx限速)
  • 用户层:防重令牌、登录态校验、防刷机制

3. 兜底与一致性

  • 定期对 Redis 库存与数据库库存做对账
  • MQ 消费失败可重试或入死信队列

五、延伸与深挖

  • 热点商品问题:库存都在一台 Redis?→ 用 Redis Cluster + Hash Tag 定向路由。
  • 超卖检测:通过 Redis incrby 判断是否越界。
  • 幂等保证:下单接口加业务唯一号(如订单token)。
  • 防止少买:使用异步确认机制,失败回补库存。

六、面试答题思路模板

如果面试官问你:

高并发下怎么避免超卖?

你可以这样答(自然流畅版):

我们项目采用 Redis 预减库存 + MQ 异步落单方案。用户请求时不直接操作数据库,而是先在 Redis 中用 Lua 脚本原子扣减库存,这样能确保高性能和原子性。扣减成功后把下单消息发送到 MQ,由异步消费者去真正创建订单并更新数据库库存。支付超时的订单会通过延时队列回滚库存。这样既能抗高并发,又能避免超卖,还能平滑削峰。