select ... for update 可防止超卖,需满足innodb引擎和索引查询,配合update带库存校验条件、read committed隔离级及死锁重试机制。

用 SELECT ... FOR UPDATE 防止超卖
在并发扣减库存场景下,不加锁直接 UPDATE 极易导致超卖。核心做法是先查再锁再改:用 SELECT ... FOR UPDATE 在事务内锁定目标行,确保同一商品不会被多个事务同时修改。
注意必须满足两个前提:表引擎为 InnoDB;查询条件命中索引(如主键或唯一索引),否则会升级为表锁,严重拖慢性能。
- 不要写
SELECT * FROM stock WHERE sku = 'A001'后再判断,这中间存在竞态窗口 - 务必把
SELECT ... FOR UPDATE和后续UPDATE放在同一个事务中(BEGIN→ 查询 → 更新 →COMMIT) - 如果查询不到记录(
sku不存在),FOR UPDATE不会报错,但后续UPDATE将影响 0 行,需主动检查ROW_COUNT()
扣减逻辑必须带库存校验条件
仅靠行锁不能替代业务校验。即使锁住了某行,也得确认当前库存是否足够——否则可能扣成负数。正确方式是在 UPDATE 语句里直接写条件判断。
UPDATE stock SET quantity = quantity - 1 WHERE sku = 'A001' AND quantity >= 1;
执行后检查 ROW_COUNT() 返回值:
- 返回
1:扣减成功 - 返回
0:库存不足或记录不存在,事务应主动回滚
避免先 SELECT quantity 再 UPDATE,那又回到竞态问题。
事务隔离级别建议用 READ COMMITTED
MySQL 默认是 REPEATABLE READ,它会使用间隙锁(Gap Lock)防止幻读,但在库存类场景中容易引发不必要的锁等待甚至死锁。而 READ COMMITTED 只锁实际命中的行,无间隙锁,更轻量。
设置方式(连接级):
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 不需要全局改,应用在扣减库存的数据库连接池初始化时设置即可
- 确认业务能接受“同一事务中两次读可能不一致”,这对下单这种短事务通常无影响
- 如果已用
SELECT ... FOR UPDATE,READ COMMITTED下锁行为更可预测,不易误锁相邻范围
别忽略连接超时和死锁重试机制
高并发下,FOR UPDATE 等待锁、网络延迟、事务过长都可能导致超时或死锁。MySQL 报错典型有:
-
Lock wait timeout exceeded(innodb_lock_wait_timeout触发) Deadlock found when trying to get lock
应用层必须捕获这些错误并实现简单重试(比如最多 3 次),而不是直接抛给用户。伪代码逻辑:
for retry in range(3):
try:
BEGIN
SELECT quantity FROM stock WHERE sku = ? FOR UPDATE;
UPDATE stock SET quantity = quantity - 1 WHERE sku = ? AND quantity >= 1;
COMMIT
break
except (DeadlockError, LockTimeoutError):
time.sleep(0.05 * (2 ** retry)) # 指数退避
continue重试前强制断开并重建连接,避免复用处于异常状态的事务上下文。










