1. 引言
在电商、秒杀、抢购等业务场景中,超卖问题是最常见也最棘手的技术挑战之一。所谓超卖,指的是系统在并发请求下,实际售出的商品数量超过了库存总量。例如,某商品库存只有 10 件,但在高并发下却卖出了 15 件,这就是典型的超卖问题。
超卖问题一旦发生,轻则造成资损和客诉,重则影响平台信誉。本文将从超卖问题的成因出发,系统梳理从数据库层面到分布式锁层面的多种解决方案,并给出不同场景下的选型建议。
2. 超卖问题的成因分析
要解决超卖问题,首先需要理解它为什么会产生。超卖的本质是并发场景下的竞态条件(Race Condition)。
2.1 典型超卖场景
假设商品库存为 10,两个用户同时发起购买请求,业务逻辑通常如下:
- 查询当前库存,得到库存为 10;
- 判断库存是否大于 0;
- 执行扣减库存,库存变为 9;
- 生成订单。
当两个请求同时执行到第 1 步时,它们都读到库存为 10,随后都通过第 2 步的判断,并各自执行扣减,最终库存可能变为 8,但实际生成了 2 个订单------如果库存只有 1,就会卖出 2 件,造成超卖。
2.2 超卖产生的根本原因
- 读改写(Read-Modify-Write)非原子操作:查询库存和扣减库存是两个独立步骤,中间存在时间窗口,并发请求会在这个窗口内读到过期数据。
- 缺乏并发控制:没有对共享资源(库存)的访问进行互斥或原子化处理。
- 事务隔离级别不足:在默认的读已提交(Read Committed)隔离级别下,无法避免上述竞态。
3. 数据库层面的解决方案
数据库是库存数据的最终存储,因此从数据库层面解决问题是最基础、最可靠的手段。
3.1 方案一:乐观锁(版本号)
乐观锁通过在库存表中增加版本号字段,在更新时校验版本号是否发生变化。
sql
-- 扣减库存前先查询
SELECT stock, version FROM product WHERE id = 100;
-- 更新时带上版本号条件
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = 5;
如果更新影响的行数为 0,说明版本号已变化,需要重试或提示用户失败。
优点 :实现简单,并发性能较好。
缺点:高并发下重试次数多,用户体验可能受影响。
3.2 方案二:悲观锁(行锁)
悲观锁通过 SELECT ... FOR UPDATE 在查询时直接锁定该行,直到事务提交或回滚才释放。
sql
-- 开启事务
BEGIN;
-- 锁定该商品行
SELECT stock FROM product WHERE id = 100 FOR UPDATE;
-- 判断库存并扣减
UPDATE product SET stock = stock - 1 WHERE id = 100;
-- 提交事务
COMMIT;
优点 :强一致性,不会出现超卖。
缺点:并发性能较差,容易造成锁等待和死锁,不适合超高并发场景。
3.3 方案三:原子更新(条件更新)
利用数据库的原子更新操作,在一条 SQL 中完成判断和扣减:
sql
UPDATE product
SET stock = stock - 1
WHERE id = 100 AND stock > 0;
如果影响行数为 0,说明库存不足。这种方式利用数据库行锁的原子性,既保证了不超卖,又比悲观锁性能更好。
优点 :实现最简单,性能优于悲观锁。
缺点:无法在扣减前做更复杂的业务校验。
4. 应用层解决方案
当数据库成为瓶颈时,需要在应用层引入额外的并发控制手段。
4.1 方案四:Redis 分布式锁
使用 Redis 的 SETNX 或 SET EX NX 命令实现分布式锁,保证同一时刻只有一个线程能执行扣减操作。
java
// 使用 Redisson 实现分布式锁
RLock lock = redissonClient.getLock("product:100:lock");
try {
// 尝试加锁,最多等待 3 秒,锁自动过期时间 10 秒
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 查询库存
int stock = getStock(100);
if (stock > 0) {
// 扣减库存
reduceStock(100);
// 创建订单
createOrder();
}
}
} finally {
lock.unlock();
}
优点 :跨进程互斥,适合分布式系统。
缺点:需要维护 Redis 高可用;锁的粒度较大时性能受影响。
4.2 方案五:Redis 原子操作(Lua 脚本)
将库存判断和扣减封装在 Lua 脚本中,利用 Redis 单线程执行脚本的原子性,避免超卖。
lua
-- 扣减库存的 Lua 脚本
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
redis.call('DECR', KEYS[1])
return 1
else
return 0
end
java
// Java 中调用 Lua 脚本
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText("..."); // 上述 Lua 脚本
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList("product:100:stock"));
if (result == 1L) {
// 扣减成功,创建订单
createOrder();
}
优点 :原子性强,性能高,是秒杀场景的常用方案。
缺点:库存数据在 Redis 中,需要处理 Redis 与数据库的一致性。
4.3 方案六:令牌桶 / 信号量限流
4.4 方案七:MQ 削峰限流
消息队列(MQ)本身并不直接解决超卖,它的核心价值在于削峰填谷------将突发的瞬时流量转化为平缓的异步处理流,从而保护下游数据库和订单系统不被流量洪峰击垮。
#mermaid-svg-5eQwuJzDJPja4xp6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5eQwuJzDJPja4xp6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5eQwuJzDJPja4xp6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5eQwuJzDJPja4xp6 .error-icon{fill:#552222;}#mermaid-svg-5eQwuJzDJPja4xp6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5eQwuJzDJPja4xp6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5eQwuJzDJPja4xp6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5eQwuJzDJPja4xp6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5eQwuJzDJPja4xp6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5eQwuJzDJPja4xp6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5eQwuJzDJPja4xp6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5eQwuJzDJPja4xp6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5eQwuJzDJPja4xp6 .marker.cross{stroke:#333333;}#mermaid-svg-5eQwuJzDJPja4xp6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5eQwuJzDJPja4xp6 p{margin:0;}#mermaid-svg-5eQwuJzDJPja4xp6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5eQwuJzDJPja4xp6 .cluster-label text{fill:#333;}#mermaid-svg-5eQwuJzDJPja4xp6 .cluster-label span{color:#333;}#mermaid-svg-5eQwuJzDJPja4xp6 .cluster-label span p{background-color:transparent;}#mermaid-svg-5eQwuJzDJPja4xp6 .label text,#mermaid-svg-5eQwuJzDJPja4xp6 span{fill:#333;color:#333;}#mermaid-svg-5eQwuJzDJPja4xp6 .node rect,#mermaid-svg-5eQwuJzDJPja4xp6 .node circle,#mermaid-svg-5eQwuJzDJPja4xp6 .node ellipse,#mermaid-svg-5eQwuJzDJPja4xp6 .node polygon,#mermaid-svg-5eQwuJzDJPja4xp6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5eQwuJzDJPja4xp6 .rough-node .label text,#mermaid-svg-5eQwuJzDJPja4xp6 .node .label text,#mermaid-svg-5eQwuJzDJPja4xp6 .image-shape .label,#mermaid-svg-5eQwuJzDJPja4xp6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5eQwuJzDJPja4xp6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5eQwuJzDJPja4xp6 .rough-node .label,#mermaid-svg-5eQwuJzDJPja4xp6 .node .label,#mermaid-svg-5eQwuJzDJPja4xp6 .image-shape .label,#mermaid-svg-5eQwuJzDJPja4xp6 .icon-shape .label{text-align:center;}#mermaid-svg-5eQwuJzDJPja4xp6 .node.clickable{cursor:pointer;}#mermaid-svg-5eQwuJzDJPja4xp6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5eQwuJzDJPja4xp6 .arrowheadPath{fill:#333333;}#mermaid-svg-5eQwuJzDJPja4xp6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5eQwuJzDJPja4xp6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5eQwuJzDJPja4xp6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5eQwuJzDJPja4xp6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5eQwuJzDJPja4xp6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5eQwuJzDJPja4xp6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5eQwuJzDJPja4xp6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5eQwuJzDJPja4xp6 .cluster text{fill:#333;}#mermaid-svg-5eQwuJzDJPja4xp6 .cluster span{color:#333;}#mermaid-svg-5eQwuJzDJPja4xp6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5eQwuJzDJPja4xp6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5eQwuJzDJPja4xp6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5eQwuJzDJPja4xp6 .icon-shape,#mermaid-svg-5eQwuJzDJPja4xp6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5eQwuJzDJPja4xp6 .icon-shape p,#mermaid-svg-5eQwuJzDJPja4xp6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5eQwuJzDJPja4xp6 .icon-shape .label rect,#mermaid-svg-5eQwuJzDJPja4xp6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5eQwuJzDJPja4xp6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5eQwuJzDJPja4xp6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5eQwuJzDJPja4xp6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户秒杀请求
MQ 消息队列
异步消费者(限速消费)
数据库扣减库存
java
// 生产者:秒杀请求先入队,立即返回"排队中"
public void seckill(Long productId, Long userId) {
// 先做前置校验(如用户是否已抢购过)
if (!checkUserCanBuy(productId, userId)) {
return;
}
// 发送 MQ 消息,异步处理
SeckillMessage msg = new SeckillMessage(productId, userId);
mqTemplate.convertAndSend("seckill.queue", msg);
// 返回"排队中,请稍后查看结果"
}
// 消费者:按固定速率消费,控制对数据库的压力
@RabbitListener(queues = "seckill.queue")
public void consume(SeckillMessage msg) {
// 消费端再做一次库存判断与扣减(原子操作)
boolean success = reduceStockWithAtomic(msg.getProductId());
if (success) {
createOrder(msg.getProductId(), msg.getUserId());
}
}
优点 :削峰填谷,保护下游系统;异步化提升系统吞吐量;天然支持流量缓冲。
缺点:MQ 本身不解决超卖,仍需在消费端配合原子扣减;引入 MQ 增加系统复杂度和消息丢失/重复消费的风险,需要做好消息可靠性与幂等处理。
在入口层对请求进行限流,控制同时进入扣减逻辑的并发数,从源头降低竞争。
java
// 使用 Guava RateLimiter 做单机限流
RateLimiter rateLimiter = RateLimiter.create(100); // 每秒 100 个请求
if (rateLimiter.tryAcquire()) {
// 进入扣减逻辑
processOrder();
} else {
// 返回"系统繁忙"
return "请稍后重试";
}
优点 :保护下游系统,防止流量洪峰。
缺点:限流本身不直接解决超卖,需要配合其他方案使用。
5. 方案对比与选型建议
| 方案 | 一致性 | 并发性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 乐观锁 | 强 | 中 | 低 | 并发量中等的普通电商 |
| 悲观锁 | 强 | 低 | 低 | 低并发、强一致要求 |
| 原子更新 | 强 | 中高 | 最低 | 简单扣减场景 |
| Redis 分布式锁 | 强 | 中 | 中 | 分布式系统、中等并发 |
| Redis Lua 脚本 | 强 | 高 | 中高 | 秒杀、高并发扣减 |
| 限流 | 弱 | 高 | 中 | 流量入口保护 |
| MQ 削峰限流 | 弱(需配合原子扣减) | 高 | 中高 | 秒杀、大流量洪峰缓冲 |
选型建议
- 普通电商(低并发):优先使用数据库原子更新或乐观锁,简单可靠。
- 秒杀/抢购(高并发):推荐 Redis Lua 脚本 + MQ 削峰 + 异步订单处理,兼顾性能与一致性。
- 分布式系统:必须使用分布式锁或 Redis 原子操作,避免多实例间的竞态。
- 混合方案:生产环境通常采用「限流 + MQ 削峰 + Redis 预扣减 + 数据库最终扣减」的组合方案。## 6. 生产环境最佳实践
6.1 库存预扣减与异步对账
在秒杀场景中,可以先在 Redis 中预扣减库存,成功后再异步落库,并通过定时任务对账,保证最终一致性。
#mermaid-svg-wJZpepVbNEO8eMpI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wJZpepVbNEO8eMpI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wJZpepVbNEO8eMpI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wJZpepVbNEO8eMpI .error-icon{fill:#552222;}#mermaid-svg-wJZpepVbNEO8eMpI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wJZpepVbNEO8eMpI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wJZpepVbNEO8eMpI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wJZpepVbNEO8eMpI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wJZpepVbNEO8eMpI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wJZpepVbNEO8eMpI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wJZpepVbNEO8eMpI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wJZpepVbNEO8eMpI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wJZpepVbNEO8eMpI .marker.cross{stroke:#333333;}#mermaid-svg-wJZpepVbNEO8eMpI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wJZpepVbNEO8eMpI p{margin:0;}#mermaid-svg-wJZpepVbNEO8eMpI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wJZpepVbNEO8eMpI .cluster-label text{fill:#333;}#mermaid-svg-wJZpepVbNEO8eMpI .cluster-label span{color:#333;}#mermaid-svg-wJZpepVbNEO8eMpI .cluster-label span p{background-color:transparent;}#mermaid-svg-wJZpepVbNEO8eMpI .label text,#mermaid-svg-wJZpepVbNEO8eMpI span{fill:#333;color:#333;}#mermaid-svg-wJZpepVbNEO8eMpI .node rect,#mermaid-svg-wJZpepVbNEO8eMpI .node circle,#mermaid-svg-wJZpepVbNEO8eMpI .node ellipse,#mermaid-svg-wJZpepVbNEO8eMpI .node polygon,#mermaid-svg-wJZpepVbNEO8eMpI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wJZpepVbNEO8eMpI .rough-node .label text,#mermaid-svg-wJZpepVbNEO8eMpI .node .label text,#mermaid-svg-wJZpepVbNEO8eMpI .image-shape .label,#mermaid-svg-wJZpepVbNEO8eMpI .icon-shape .label{text-anchor:middle;}#mermaid-svg-wJZpepVbNEO8eMpI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wJZpepVbNEO8eMpI .rough-node .label,#mermaid-svg-wJZpepVbNEO8eMpI .node .label,#mermaid-svg-wJZpepVbNEO8eMpI .image-shape .label,#mermaid-svg-wJZpepVbNEO8eMpI .icon-shape .label{text-align:center;}#mermaid-svg-wJZpepVbNEO8eMpI .node.clickable{cursor:pointer;}#mermaid-svg-wJZpepVbNEO8eMpI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wJZpepVbNEO8eMpI .arrowheadPath{fill:#333333;}#mermaid-svg-wJZpepVbNEO8eMpI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wJZpepVbNEO8eMpI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wJZpepVbNEO8eMpI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wJZpepVbNEO8eMpI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wJZpepVbNEO8eMpI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wJZpepVbNEO8eMpI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wJZpepVbNEO8eMpI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wJZpepVbNEO8eMpI .cluster text{fill:#333;}#mermaid-svg-wJZpepVbNEO8eMpI .cluster span{color:#333;}#mermaid-svg-wJZpepVbNEO8eMpI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wJZpepVbNEO8eMpI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wJZpepVbNEO8eMpI rect.text{fill:none;stroke-width:0;}#mermaid-svg-wJZpepVbNEO8eMpI .icon-shape,#mermaid-svg-wJZpepVbNEO8eMpI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wJZpepVbNEO8eMpI .icon-shape p,#mermaid-svg-wJZpepVbNEO8eMpI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wJZpepVbNEO8eMpI .icon-shape .label rect,#mermaid-svg-wJZpepVbNEO8eMpI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wJZpepVbNEO8eMpI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wJZpepVbNEO8eMpI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wJZpepVbNEO8eMpI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败
用户发起秒杀请求
Redis Lua 预扣减库存
扣减是否成功
发送 MQ 消息
返回已售罄
异步消费:生成订单 + 数据库扣减
定时对账:Redis 与 DB 库存一致性校验
6.2 防重提交
同一用户重复点击提交按钮,需要通过用户维度的幂等控制来避免重复下单:
java
// 使用 Redis SETNX 做幂等控制
Boolean first = redisTemplate.opsForValue()
.setIfAbsent("order:user:1001", "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(first)) {
// 首次提交,执行下单逻辑
processOrder();
} else {
// 重复提交,直接返回
return "请勿重复提交";
}
6.3 库存预热与缓存
活动开始前将库存预热到 Redis,减少活动期间对数据库的直接压力;活动结束后将最终库存回写数据库。
7. 总结
超卖问题的本质是并发下的竞态条件,解决方案的核心思路是让「判断库存」和「扣减库存」成为一个原子操作。
- 数据库层面:乐观锁、悲观锁、原子更新;
- 应用层层面:分布式锁、Redis Lua 脚本、限流、MQ 削峰;
- 生产实践:预扣减 + MQ 异步化 + 对账 + 幂等控制。实际项目中,没有银弹,需要根据并发量、一致性要求、团队技术栈综合选型。建议从最简单的数据库原子更新入手,在性能出现瓶颈时再逐步引入 Redis 方案,并配合限流和异步化,构建一套高可用、不超卖的库存系统。