订阅更新

高并发下单时库存超卖了,怎么用MySQL保证不超卖?

MySQL手册 第 5 题 约 4 分钟读完

本文目录
  1. 先说结论
  2. 为什么“先查再扣”会超卖?
  3. 方案一:悲观锁(SELECT FOR UPDATE)
  4. 方案二:乐观锁(版本号控制)
  5. 方案三:WHERE条件扣减(最实用★)
  6. 实战踩坑:unsigned字段的额外保护
  7. 总结

先说结论

用MySQL保证不超卖的核心是:把“查询库存 + 扣减库存”变成一个原子操作。有三种实现方式,从简单到复杂:悲观锁、乐观锁、WHERE条件扣减。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

库存超卖是电商/交易场景里最经典的问题。看起来简单——“先查库存再扣减”——但在高并发下这样做会超卖。今天把从“为什么会超卖”到“怎么保证不超卖”彻底讲清楚。

为什么“先查再扣”会超卖?

-- ✖ 会超卖的写法
-- 并发场景:库存只剩1件,两个请求同时到达
 
-- 请求A                       -- 请求B
SELECT stock FROM product      SELECT stock FROM product
WHERE id=1;  -- 查到stock=1    WHERE id=1;  -- 查到stock=1
 
-- 两个请求都认为库存足够,都执行扣减
UPDATE product SET stock=stock-1 WHERE id=1;  -- stock变成0
UPDATE product SET stock=stock-1 WHERE id=1;  -- stock变成-1!超卖了

问题在于:SELECT和UPDATE之间有时间差,别的事务可以插进来。

方案一:悲观锁(SELECT FOR UPDATE)

BEGIN;
-- 加排他锁,其他事务读写都要等待
SELECT stock FROM product WHERE id=1 FOR UPDATE;
 
-- 检查库存
-- if (stock > 0) {
UPDATE product SET stock = stock - 1 WHERE id = 1;
INSERT INTO orders (product_id, ...) VALUES (1, ...);
-- }
COMMIT;

优点:简单可靠。缺点:并发低,所有请求排队等待锁。适合并发量不高的场景。

方案二:乐观锁(版本号控制)

-- 表加一个version字段
SELECT stock, version FROM product WHERE id = 1;
-- 假设查到 stock=5, version=10
 
-- 更新时带上version条件
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 10;  -- 只有version没变才能更新成功
 
-- 如果影响行数=0,说明别人已经改过了,需要重试或报错

优点:不加锁,并发高。缺点:强冲突时重试多。适合读多写少的场景。

方案三:WHERE条件扣减(最实用★)

这是我在生产环境最常用的方案,也是最优雅的:

-- 把“查询+扣减”合并成一条原子SQL
UPDATE product
SET stock = stock - 1
WHERE id = 1 AND stock > 0;  -- 关键:stock > 0保证不会扣成负数
 
-- 检查影响行数
-- if (affectedRows == 1) 下单成功
-- if (affectedRows == 0) 库存不足,返回“已售罄”

为什么这样不会超卖?因为UPDATE是原子的,MySQL会在执行UPDATE时自动加行锁。stock > 0这个条件保证了即使并发执行,也不会把stock扣成负数。

优点:一条SQL搞定,不需要事务,性能最好。我在元宇宙社交App的虚拟礼物库存用的就是这个方案。

实战踩坑:unsigned字段的额外保护

如果stock字段是UNSIGNED INT,即使不加stock > 0条件,扣成负数时MySQL也会报错而不是静默成功。但不能依赖这个——应用层必须自己做检查。

另外,如果一次要扣减多件(比如购买3件),WHERE条件要写成stock >= 3,不是stock > 0。这个细节很多人忽略。

总结

方案原理并发性能适用场景
悲观锁SELECT FOR UPDATE加行锁低(排队等待)并发不高的场景
乐观锁version字段控制中(强冲突时重试多)读多写少
WHERE条件扣减stock > 0原子检查高(一条SQL)库存扣减场景首选

面试时能说清楚“为什么先查再扣会超卖”,再给出三种方案并分析各自适用场景,就很完整了。

第1-5篇完结。后续将继续聊事务隔离级别、B+树索引原理、联合索引设计、Binlog应用场景、读写分离架构等实战场景。

你们生产环境怎么防止超卖的?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器