select for update 不能单独防止超卖,必须配合显式事务、主键或唯一索引、update时库存校验(where id = ? and stock >= 1)及row_count()检查,且避免事务中耗时操作和死锁。

SELECT FOR UPDATE 本身不能防止超卖,必须配合事务控制、索引约束和原子更新逻辑才能生效。
SELECT FOR UPDATE 必须在显式事务中执行
autocommit=1(默认)下,SELECT ... FOR UPDATE 执行完就自动提交,锁立即释放,后续 UPDATE 实际上没被锁保护。这会导致两个事务都查到 stock=1,都执行减库存,最终变成 -1。
- 必须显式写
BEGIN或START TRANSACTION开启事务 - 所有操作(
SELECT FOR UPDATE、应用层判断、UPDATE)必须在同一个事务内 - 不能中途
COMMIT或隐式提交(比如某些 ORM 的 save() 自动 commit) - 连接配置要确认
autocommit=0,否则BEGIN可能无效
id 必须是主键或唯一索引,否则可能锁表
InnoDB 的 SELECT ... FOR UPDATE 只对匹配的行加行锁,前提是优化器能走索引。如果 WHERE id = ? 中的 id 字段没有索引,InnoDB 会退化为全表扫描 + 表级锁,高并发下直接拖垮数据库。
- 检查执行计划:
EXPLAIN SELECT stock FROM products WHERE id = 123 FOR UPDATE;,确保type是const或eq_ref - 非主键字段(如
sku)必须建唯一索引,否则FOR UPDATE不保证只锁一行 - 避免用
LIKE、函数、或非确定性条件(如WHERE stock > 0),这些无法精确定位,易触发间隙锁或更宽泛的锁定范围
UPDATE 必须带库存校验条件,不能只靠应用层 if 判断
即使加了 FOR UPDATE,如果 UPDATE 不带 stock >= 1 条件,仍可能超卖:比如事务 A 查到 stock=1 → 应用层判断通过 → 睡眠 100ms → 事务 B 已完成扣减并提交 → 事务 A 的 UPDATE products SET stock = stock - 1 WHERE id = 123 仍会执行,把 stock 变成 0 → 实际已售罄,但未拦截。
- 务必在
UPDATE的WHERE子句里重复校验:WHERE id = 123 AND stock >= 1 - 执行后检查
ROW_COUNT():返回 0 表示没更新成功(库存不足或记录不存在),不是异常,是预期结果 - 不要省略这个条件去依赖“锁住就一定够”,锁只保“读不被干扰”,不保“写时状态未变”
锁持有时间越短越好,别在事务里做耗时操作
锁持续时间 = 事务从 BEGIN 到 COMMIT 的全部时间。中间调用 RPC、发邮件、处理图片、甚至 sleep(0.1),都会让锁卡住其他请求,堆积大量等待线程,最终触发连接超时或死锁。
- 把库存判断和更新逻辑尽量压在数据库层,减少应用层计算环节
- 避免在事务中调用外部服务;如有必要,先
COMMIT释放锁,再异步处理后续动作 - 注意 ORM 框架是否悄悄做了额外查询(比如 N+1 查询),它们也会延长事务时间
- 死锁真实存在:A 锁 id=1 后想锁 id=2,B 锁 id=2 后想锁 id=1 —— 这种顺序不一致极易发生,尤其多商品下单时
真正难的不是写那条 SELECT ... FOR UPDATE,而是确保它运行在正确的上下文里:事务边界清晰、索引可用、更新条件完备、锁时间可控。漏掉任意一环,它就只是个看起来很安全的幻觉。











