mysql update死锁根本原因是多个事务以不同顺序争抢索引锁形成循环等待;innodb自动回滚代价小的事务并报错“deadlock found when trying to get lock”;需通过show engine innodb status定位,预防关键在于统一加锁顺序、确保索引命中、避免混用索引路径。

UPDATE 导致死锁,根本不是“SQL写错了”,而是多个事务在索引上以不同顺序争抢锁,最终形成循环等待。MySQL 会主动杀掉一个事务(报错 Deadlock found when trying to get lock),但你不该只等它报错才行动。
为什么 UPDATE 容易触发死锁?关键在索引加锁顺序不一致
MySQL 行锁本质是锁索引,不是锁记录。一条 UPDATE 若涉及非主键索引,InnoDB 会分步加锁:先锁非主键索引项,再回表锁主键索引项。而另一条走主键条件的 UPDATE 则相反:先锁主键,再锁相关二级索引。两者交错执行,就卡住了。
- 事务 A 执行
UPDATE t SET status=1 WHERE name='alice'→ 先锁name索引,再锁对应主键 - 事务 B 同时执行
UPDATE t SET status=2 WHERE id=1005→ 先锁主键id=1005,再锁name索引(因为要更新status,可能触发二级索引维护) - 若 A 已持
name='alice'锁、B 已持id=1005锁,且这两行恰好是同一记录——A 等 B 释放主键锁,B 等 A 释放name锁,死锁成立
这种场景在多线程批量更新、分页任务重试、或模糊条件(LIKE、范围查询)中高频出现。
怎么快速确认是不是死锁?别只看应用日志
应用层捕获到 Deadlock found when trying to get lock(MySQL 错误码 1213)只是结果。真正要定位,得进数据库查实时上下文:
- 立刻执行
SHOW ENGINE INNODB STATUS\G,翻到LATEST DETECTED DEADLOCK段——它会明确写出两个事务各自执行的 SQL、持有的锁、等待的锁、甚至事务堆栈 - 检查错误日志是否开启
innodb_print_all_deadlocks = ON(推荐长期打开),所有死锁都会落盘,方便回溯趋势 - 避免只查
information_schema.INNODB_TRX:它只显示当前运行事务,死锁发生后肇事事务已被回滚,看不到现场
注意:SHOW PROCESSLIST 和 performance_schema.events_statements_current 对死锁排查帮助有限,它们不记录锁等待链。
如何从代码和 SQL 层面预防?重点不是“加锁”,而是“锁序统一”
与其在事务里加各种 SELECT ... FOR UPDATE 控制粒度,不如让所有业务路径对同一组数据按固定顺序加锁:
- 批量更新必须排序:把
UPDATE t SET x=1 WHERE id IN (1005, 1002, 1008)改成UPDATE t SET x=1 WHERE id IN (1002, 1005, 1008) ORDER BY id(MySQL 8.0+ 支持ORDER BY在UPDATE中生效) - 禁止用非主键条件做高并发更新:如必须按
email更新,先SELECT id FROM t WHERE email=? FOR UPDATE拿到主键,再用UPDATE t SET ... WHERE id=? - 避免在事务中混用不同索引路径:同一个业务逻辑里,不要一会儿用
WHERE order_no=...,一会儿又用WHERE user_id=...去更新同一张表的关联行 - 长事务必须拆分:两千万行更新别写
UPDATE big_table SET flag=1一把梭,改用WHERE id BETWEEN ? AND ?分批,每批提交后释放锁
最常被忽略的一点:即使你用了主键更新,如果表上有唯一二级索引且 UPDATE 修改了该索引列(比如 UPDATE t SET email='new@x' WHERE id=1005),InnoDB 仍需锁住旧值和新值对应的二级索引项——这也会引入额外锁点,增加冲突面。










