死锁不是由单条update语句导致,而是多个事务因索引扫描路径不同、加锁顺序不一致(如a按name→pk加锁、b按email→pk→name加锁)形成循环等待所致;关键在加锁路径是否可预测和统一。

Update 语句本身不会“导致”死锁,死锁是多个事务交叉加锁、顺序不一致时的必然结果。关键在加锁路径是否可预测、是否统一。
UPDATE 加锁顺序由索引扫描路径决定
InnoDB 对 UPDATE 的执行分两步:先按 WHERE 条件做当前读(加锁),再执行更新。加什么锁、加在哪、顺序如何,完全取决于优化器选的索引和扫描方式。
- 如果
WHERE走ind_name索引 → 加锁顺序是:ind_name行锁 → 聚簇索引(主键)行锁 - 如果另一个事务的
DELETE走ind_email索引 → 加锁顺序是:ind_email行锁 → 聚簇索引行锁 →ind_name行锁 - 两者在聚簇索引和二级索引之间形成反向加锁链,一并发就死锁
执行计划里出现 Index Scan 或 Using where; Using index condition 并不等于安全——只要扫描路径不同,锁顺序就可能冲突。
ORDER BY 和 LIMIT 在子查询里会破坏锁顺序可控性
写 UPDATE t1 SET x=1 WHERE id IN (SELECT id FROM t2 ORDER BY created_at DESC LIMIT 10) 是高危操作。MySQL 5.7+ 默认物化子查询,生成临时表后排序,导致:
- 全表扫描
t2(哪怕created_at有索引,也可能因物化放弃使用) - 临时表无主键顺序保障,外层
IN更新时加锁顺序随机 - 同一组 ID,在事务 A 中按 9→5→1 加锁,事务 B 按 1→5→9 加锁,立刻成环
正确做法是应用层或子查询中显式 ORDER BY id,确保拿到升序 ID 列表后再拼进 WHERE id IN ()。
SQL Server 中非聚集索引的 INCLUDE 字段会放大死锁风险
当 UPDATE 涉及带 INCLUDE 的非聚集索引(如 CREATE INDEX ix_a ON tt(a) INCLUDE(d)),且 d 是 varchar(max) 时,引擎可能在更新过程中触发额外的锁请求:
- 先锁
ix_a上匹配的行 - 再回表取聚簇索引数据(需要锁主键)
- 最后还要更新
INCLUDE列值 → 若该列存储在溢出页(LOB),可能引发额外页锁甚至锁升级
去掉 INCLUDE(d) 或把 d 改为固定长度(如 varchar(200)),就能让更新只走索引查找+聚簇更新两步,锁路径收敛、可预期。
最易被忽略的一点:死锁现场日志里看到的 SQL 往往是“干净”的,但真正决定锁行为的是它背后隐式的索引选择、物化策略、以及是否触发锁升级——这些不会出现在语句文本里,只能靠 EXPLAIN、sys.dm_exec_query_plan 或死锁图里的 resource-list 去还原。










