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

如何解决高并发下的库存抢购超卖少买问题?
北川一、问题本质
“超卖”和“少买”是并发修改库存导致的数据不一致问题。
- 超卖:库存 1 件,却卖出 2 件(库存减到负数)
- 少买:库存足够,但有线程被锁或排队失败,导致明明能买却没买成功。
根因是:多个线程几乎同时读取相同库存值,然后同时修改,导致覆盖更新。
比如:
库存=1 |
二、常见解决思路总览
我们可以从三个层面看:
| 层面 | 核心手段 | 举例 |
|---|---|---|
| 数据库层面 | 行锁、悲观锁、乐观锁 | for update、version 字段 |
| 缓存/队列层面 | 异步削峰、单线程顺序消费 | Redis + MQ |
| 分布式协调层面 | 分布式锁、防重令牌 | Redis、Zookeeper |
三、具体实现方案
1. 悲观锁(数据库行锁)
做法:
select stock from product where id = 1 for update; |
这种方式在 MySQL InnoDB 下会锁住那一行,直到事务提交。
优点:
简单直观,适合低并发场景。
缺点:
锁粒度大,性能差;在高并发下大量阻塞或死锁。
适用:
中小系统或后台管理类接口。
2. 乐观锁(version 机制)
原理:
给库存表加 version 字段,每次修改时判断版本号是否一致:
update product set stock = stock - 1, version = version + 1 |
若返回行数为 1 → 成功,否则重试。
优点:
无锁操作,高并发性能好。
缺点:
需要重试逻辑,可能失败率高(但可通过限流、MQ 缓解)。
实战建议:
结合 Redis 限流或消息队列使用。
3. Redis + Lua 脚本(缓存库存扣减)
核心思路:
库存不查数据库,直接在 Redis 中操作:
-- lua原子操作示例 |
优点:
原子性强(Lua脚本在 Redis 单线程中执行),速度极快。
缺点:
需要定时同步库存回数据库(防止 Redis 宕机丢数据)。
实战优化:
- Redis 预减库存 + MQ 异步扣减数据库。
- 超时未支付的订单库存回滚。
4. 消息队列(MQ)削峰 + 异步扣减
架构思路:
用户请求 → 放入 MQ 队列 → 消费者顺序处理下单逻辑(扣库存、生成订单)
好处:
削峰填谷,系统不直接承压。
关键点:
- 队列消费要幂等(防止重复消费)
- 消费失败要有重试机制
- 可结合 Redis 预减库存(先扣库存,再入队)
典型组合:
Redis 预减库存 + RabbitMQ 异步落单。
5. 分布式锁(Redis / Zookeeper)
实现例:
使用 Redis SETNX 加锁:
String key = "lock:product:" + productId; |
拿到锁才能扣减库存。
问题:
- 性能不如 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,由异步消费者去真正创建订单并更新数据库库存。支付超时的订单会通过延时队列回滚库存。这样既能抗高并发,又能避免超卖,还能平滑削峰。








