limit加for update本身不会导致死锁,但where无索引会引发全表扫描加锁、非唯一索引下间隙锁仍生效、批量查询中加锁顺序混乱,三者共同构成死锁主因。

LIMIT 加 FOR UPDATE 不会导致死锁——但没加对、没用对、或者误以为它能防死锁,就会引发死锁。 它本身不是死锁诱因,而是掩盖索引缺失、执行计划失控、间隙锁滥用等问题的“烟雾弹”。真正出问题的,永远是 WHERE 条件没走索引、扫描范围失控、或多个事务在相同间隙上反复争抢。
WHERE 条件没索引时,LIMIT 1 也救不了全表扫描加锁
当 SELECT ... FOR UPDATE LIMIT 1 的 WHERE 字段无有效索引(比如 status = 'pending' 没建索引),InnoDB 会全表扫描。哪怕只返回 1 行,所有被扫描过的行都会被加上 X 锁——LIMIT 1 只是让扫描提前终止,但前面扫过的几千行锁已加完。此时两个并发事务都执行该语句,极大概率锁住大量重叠行,后续更新一碰就死锁。
- 必须用
EXPLAIN FORMAT=TRADITIONAL验证type是const或ref,且rows接近 1 - 如果
Extra出现Using where; Using filesort或Using temporary,说明索引失效,LIMIT形同虚设 - 常见陷阱:隐式类型转换(
WHERE id = '123'而id是INT)、函数包裹字段(WHERE UPPER(name) = 'ABC')
非唯一索引 + LIMIT 1 → 间隙锁照常生效,死锁风险不降反升
在 RR 隔离级别下,SELECT ... FOR UPDATE 对非唯一索引字段(如 order_no)做等值查询时,InnoDB 会加 Next-Key Lock(记录锁 + 间隙锁)。即使加了 LIMIT 1,只要查不到记录,就会锁住整个间隙;若查到记录,仍可能锁住右侧间隙。多个事务同时执行 SELECT order_no = ? FOR UPDATE LIMIT 1,会反复争抢同一间隙,再叠加后续 INSERT 的插入意向锁,极易形成死锁链。
- 例如表中
order_no有值 100、200,执行SELECT ... WHERE order_no = 150 FOR UPDATE LIMIT 1→ 锁住间隙(100, 200) - 此时另一个事务插入
order_no = 150或180,就会被阻塞;若它也正试图锁同一间隙,死锁立即触发 -
LIMIT 1对间隙锁范围毫无影响——间隙锁是按索引结构推导出来的,不是按结果行数裁剪的
批量场景下,LIMIT 让加锁顺序更不可控
很多人想用 UPDATE ... WHERE status = 'pending' ORDER BY id LIMIT 100 FOR UPDATE(语法错误,FOR UPDATE 不能直接用于 UPDATE)来分批处理,实际却写成 SELECT id FROM t WHERE status = 'pending' ORDER BY id LIMIT 100 FOR UPDATE,再拿 ID 去更新。问题在于:ORDER BY id 是否真走索引?如果 status 没索引,优化器大概率全表扫描后 filesort,LIMIT 100 返回的 ID 顺序完全随机——事务 A 拿到 [5, 1, 9],事务 B 拿到 [9, 5, 1],后续更新顺序天然相反,死锁条件齐备。
- 正确做法是确保
WHERE和ORDER BY共用联合索引,例如(status, id) - 验证方式:
EXPLAIN中key显示该联合索引,Extra无Using filesort - 更稳妥的是游标式分页:
SELECT id FROM t WHERE status = 'pending' AND id > 15000 ORDER BY id LIMIT 100 FOR UPDATE,每次记录上一批最大id
真正容易被忽略的点是:FOR UPDATE 的锁持续到事务结束,而 LIMIT 只影响语句执行路径。如果你在事务里先执行带 LIMIT 的 SELECT FOR UPDATE,又在后续语句中因逻辑错误去查/更新其他行,锁持有时间拉长、范围扩散,死锁概率只增不减。别把 LIMIT 当安全带,它只是刹车片——踩得准才有用,踩歪了反而更危险。











