死锁主因是多事务对同一组资源加锁顺序不一致,而非UPDATE或SELECT FOR UPDATE本身;须统一加锁路径,确保WHERE走主键或唯一索引,用EXPLAIN验证执行计划,避免非主键索引回表与间隙锁叠加引发循环等待。

UPDATE 本身不会“引发”死锁,死锁是多个事务对同一组资源以不同顺序加锁时必然出现的循环等待——关键在加锁路径是否统一、可预测。
为什么EXPLAIN比ORDER BY更值得信任
很多人加了 ORDER BY id 就以为锁顺序安全了,但 MySQL 实际加锁顺序完全由执行计划决定。如果 WHERE 条件没走索引,优化器可能全表扫描 + Using filesort,结果锁住所有匹配行,且顺序随机。
- 必须用
EXPLAIN FORMAT=TRADITIONAL验证:看key是否命中预期索引,Extra是否含Using filesort - 复合索引要满足最左前缀:有
(status, id)索引时,WHERE status = 'pending' ORDER BY id才生效;WHERE id > 100则基本无效 -
ORDER BY在子查询里更危险:UPDATE ... WHERE id IN (SELECT ... ORDER BY created_at LIMIT 10)可能触发物化临时表,外层加锁顺序完全失控
非主键条件更新为何更容易死锁
当 WHERE 走非主键索引(如 ind_email)时,InnoDB 加锁分两步:先锁二级索引项,再回表锁主键。这中间存在时间窗口,若另一事务正用主键直接更新同一行,就容易形成反向加锁链。
- 例如事务 A 执行
UPDATE t SET x=1 WHERE email='a@b.com'→ 先锁ind_email行,再锁主键 - 事务 B 同时执行
UPDATE t SET x=2 WHERE id=123→ 直接锁主键,再可能触发二级索引维护 - 两者在主键和二级索引之间形成循环等待
- 解法:优先让批量更新走主键;若必须用非主键字段,确保该字段有唯一索引,缩小锁范围
SELECT FOR UPDATE 是个高危操作
在 RR 隔离级别下,SELECT ... FOR UPDATE 即使查不到记录,也会对查询范围加间隙锁(Gap Lock)。多个并发请求争抢同一间隙,再叠加后续 INSERT 的插入意向锁,立刻构成死锁链。
- 错误示例:
SELECT * FROM orders WHERE order_no = 'ABC' FOR UPDATE→ 记录不存在 → 所有并发都卡在同一间隙 - 正确替代:
INSERT INTO orders (...) VALUES (...) ON DUPLICATE KEY UPDATE updated_at = NOW(),前提是order_no有UNIQUE约束 - 若必须用
SELECT FOR UPDATE,确保WHERE字段有唯一索引,且EXPLAIN显示type = const或ref、rows = 1 - 绝对避免对重复率高的字段(如
name)做SELECT FOR UPDATE
批量UPDATE的分批策略不能只靠LIMIT
LIMIT 在 UPDATE 中只限制影响行数,不提供稳定分片语义。当其他事务正在写入时,“第 2 批 100 条”可能和上一批重叠或跳过某些记录,导致加锁顺序不可控。
- OFFSET 分页式分批(如
LIMIT 1000 OFFSET 1000)在高并发下极易错乱 - 推荐游标式更新:每次记录上一批最大
id值作为下一批起点,例如WHERE id > 15000 AND id <= 20000 - 若需按状态筛选,先用
SELECT id FROM t WHERE status = 'pending' ORDER BY id LIMIT 5000拿出 ID 列表,再WHERE id IN (...)更新——注意max_allowed_packet和 IN 列表长度限制
真正卡住人的地方,往往不是“要不要加索引”,而是“加了索引但执行计划没走”;也不是“有没有 ORDER BY”,而是“ORDER BY 被优化器无视了”。死锁日志里看到的 SQL 很干净,但背后隐式的索引选择、物化策略、锁升级行为,只能靠 EXPLAIN、死锁图里的 resource-list 或 sys.dm_exec_query_plan 去还原。

















