优先用原子UPDATE WHERE校验,避免先查后更新导致超卖;执行后检查影响行数为0即说明库存不足或已被扣减,无需加锁且不依赖隔离级别,适用于条件可直接写入WHERE的简单场景。

直接用 UPDATE WHERE 做原子校验,别先查再更新
高并发下最常踩的坑,就是写成两步:SELECT stock FROM inventory WHERE sku = ? → 判断够不够 → UPDATE inventory SET stock = stock - 1 WHERE sku = ?。这两步之间存在毫秒级窗口,两个请求同时查到 stock = 10,都会执行扣减,结果变成 8,超卖已发生。
真正防超卖的写法只有一条语句:UPDATE inventory SET stock = stock - @num WHERE sku = @sku AND stock >= @num。数据库内部一次完成“判断 + 修改”,不存在中间状态。执行后必须立刻检查影响行数:ROW_COUNT() 返回 0,就说明条件不满足——要么库存不足,要么已被别人抢先扣完。
适用前提很明确:业务逻辑能全部塞进 WHERE 条件里。比如“扣 3 件”、“余额 ≥ 50 元”、“状态为 pending”。不适用于要查多张表、调外部服务、或做复杂计算的场景。
必须配合事务,且失败时显式回滚或中断
哪怕用了原子 UPDATE WHERE,如果后续还有插入订单、更新销量等操作,整个流程仍需包裹在事务中。否则成功扣了库存但订单没建出来,数据就断层了。
常见错误是只做判断却不终止流程:IF ROW_COUNT() = 0 THEN SELECT 'stock not enough' AS msg; END IF;
这只会返回提示,事务还在开着,后面语句照常执行,最后可能 COMMIT 一个半截订单。
正确做法是:
- 用 SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'stock not enough' 主动报错,触发自动回滚
- 或手动 ROLLBACK 后 LEAVE proc_label(需提前定义 proc_label:)
- 别在失败路径里写日志表——失败就不该留任何副作用
SELECT FOR UPDATE 仅在必要时用,且锁必须精准
只有当业务逻辑无法单条 SQL 表达(比如要读取商品价格、用户等级、优惠规则后再决定扣多少),才考虑加锁。此时第一句必须是:SELECT stock, price INTO @cur_stock, @price FROM inventory WHERE sku = @sku FOR UPDATE。
三个关键点:
- FOR UPDATE 必须在 BEGIN TRANSACTION 之后立即执行,单独执行无效
- WHERE 字段(如 sku)必须有唯一索引,否则可能锁表或锁住不相关记录
- 锁持有时间越短越好,查完立刻校验并更新,别在锁住期间调 API、算税费、发消息
别碰 GET_LOCK() 和 sp_getapplock 做库存互斥
有人想绕开数据库行锁,用 GET_LOCK('stock_123', 0) 或 SQL Server 的 sp_getapplock。这反而更危险:
- GET_LOCK 是会话级,连接池复用后旧锁残留,新请求被静默阻塞
- 锁名拼接出错(比如 sku 含特殊字符)会导致不同商品锁冲突或漏锁
- 它和事务不绑定,锁住了但数据可能已被其他事务改掉,一致性无法保障
- 没有死锁检测,容易卡死,排查困难
真正需要锁的时候,就老实用 SELECT ... FOR UPDATE;不需要锁的时候,优先走原子 UPDATE WHERE。这是 MySQL 库存场景里最稳的两条路。
最容易被忽略的是:原子 UPDATE WHERE 看似简单,但业务侧必须确保所有扣减入口都走同一套逻辑,不能一部分走存储过程,一部分走应用层 SQL,也不能一部分用 quantity = quantity - 1,另一部分用 quantity = 100 直接赋值——触发器或校验逻辑会失效。

















