
线上接口突然报出 Deadlock found when trying to get lock,不少人的第一反应是重启 MySQL、调大锁等待时间,或者直接终止某个连接。实际上,InnoDB 发现死锁后通常已经主动回滚了其中一个事务,让另一方继续执行。此时最重要的不是“解锁”,而是尽快保存证据,找出两个事务为什么会形成循环等待。
死锁并不等于数据库性能差。只要系统存在并发写入和多条记录锁定,就有可能发生死锁。真正需要处理的是频繁出现、影响核心接口,或者重试后仍持续失败的死锁。
本文以 MySQL InnoDB 为例,介绍如何读取死锁日志、定位等待链、检查 SQL 与索引,并从事务顺序和应用重试两个方向完成修复。
一、先分清死锁和锁等待超时
MySQL 中最常见的两类锁错误是 1213 和 1205,它们看起来相似,处理方式却不同。
错误1213:检测到死锁
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
死锁表示两个或多个事务形成了循环等待。例如,事务 A 已锁住记录 1,等待记录 2;事务 B 已锁住记录 2,又等待记录 1。双方都无法继续,InnoDB 会选择一个事务作为牺牲者并回滚,以打破循环。
错误1205:锁等待超时
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
锁等待超时表示某个事务长时间等不到锁,但不一定形成循环。可能只是另一个事务执行太久、忘记提交,或者正在等待外部服务。默认情况下,锁等待超时通常只回滚当前语句,而不是自动回滚整个事务,因此应用必须明确执行 ROLLBACK,不能让一个只执行了一半的事务继续运行。
调大 innodb_lock_wait_timeout 只能延长普通锁等待的时间,不能解决死锁。死锁检测不需要等到超时,一旦形成循环,InnoDB 就可能立即回滚其中一个事务。
二、第一时间保存最近一次死锁报告
出现 1213 后,先执行:
SHOW ENGINE INNODB STATUS\G |
在输出中找到 LATEST DETECTED DEADLOCK。这里通常会列出:
- 参与死锁的事务编号;
- 每个事务正在执行的 SQL;
- 已经持有的锁;
- 正在等待的锁;
- 涉及的表、索引和记录;
- 哪个事务被回滚。
这份报告只保留最近一次检测到的死锁。高并发环境中,下一次死锁可能很快覆盖上一份记录,所以不要等问题结束后再查。应用日志出现 1213 时,最好立即触发采集,或者暂时开启全部死锁日志。
查看状态时可以把结果写入文件:
mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G" > /tmp/innodb-status.txt |
文件中可能包含表名、SQL 参数和业务数据,提交到工单或发送给他人前应先脱敏。
三、让所有死锁写入MySQL错误日志
如果死锁偶发、难以现场捕获,可以临时启用:
SET GLOBAL innodb_print_all_deadlocks = ON; |
启用后,InnoDB 检测到的每次死锁都会写入 MySQL 错误日志。先确认错误日志位置:
SHOW VARIABLES LIKE 'log_error'; |
如果 MySQL 由 systemd 管理,也可以结合安装方式查看服务日志:
sudo journalctl -u mysqld --since "1 hour ago" |
部分系统的服务名是 mysql:
sudo journalctl -u mysql --since "1 hour ago" |
采集到足够样本后,如果不需要长期记录,可以关闭该选项,避免错误日志被大量重复死锁信息占满:
SET GLOBAL innodb_print_all_deadlocks = OFF; |
生产环境修改全局变量需要相应权限。还要注意,运行时设置通常不会替代配置管理;是否需要持久化,应根据 MySQL 版本和运维方式决定。
四、用Performance Schema查看当前锁等待链
死锁发生后,牺牲事务已经被回滚,等待环会随之消失。Performance Schema 更适合观察“此刻仍然存在”的锁等待关系,而不是还原已经结束的历史死锁。
可以使用 data_lock_waits 关联请求锁和阻塞锁:
SELECT r.ENGINE_TRANSACTION_ID AS waiting_trx_id, r.OBJECT_SCHEMA, r.OBJECT_NAME, r.INDEX_NAME, r.LOCK_TYPE AS waiting_lock_type, r.LOCK_MODE AS waiting_lock_mode, b.ENGINE_TRANSACTION_ID AS blocking_trx_id, b.LOCK_MODE AS blocking_lock_mode FROM performance_schema.data_lock_waits AS w JOIN performance_schema.data_locks AS r ON r.ENGINE = w.ENGINE AND r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID JOIN performance_schema.data_locks AS b ON b.ENGINE = w.ENGINE AND b.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID\G |
如果还想查看等待线程和阻塞线程当前正在执行什么,可以查询线程信息:
SELECT w.REQUESTING_THREAD_ID, req.PROCESSLIST_ID AS waiting_process_id, req.PROCESSLIST_INFO AS waiting_sql, w.BLOCKING_THREAD_ID, blk.PROCESSLIST_ID AS blocking_process_id, blk.PROCESSLIST_INFO AS blocking_sql FROM performance_schema.data_lock_waits AS w LEFT JOIN performance_schema.threads AS req ON req.THREAD_ID = w.REQUESTING_THREAD_ID LEFT JOIN performance_schema.threads AS blk ON blk.THREAD_ID = w.BLOCKING_THREAD_ID\G |
PROCESSLIST_INFO 只代表线程当前可见的语句,可能为空,也不一定就是最初持锁的那条 SQL。定位长事务时,还应结合事务开始时间、业务日志和调用链判断,不能只凭当前语句下结论。
五、用一个最小示例理解死锁
假设 accounts 表中存在主键为 1 和 2 的两条记录。
会话 A 先锁记录 1:
START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; |
会话 B 先锁记录 2:
START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 2; |
随后,会话 A 尝试更新记录 2,它会等待会话 B:
UPDATE accounts SET balance = balance + 100 WHERE id = 2; |
此时会话 B 再更新记录 1:
UPDATE accounts SET balance = balance + 100 WHERE id = 1; |
循环等待形成后,其中一个会话会收到 1213。这个例子说明,单条 SQL 本身没有明显错误,问题来自两个事务获取资源的顺序不一致。
六、怎样读懂LATEST DETECTED DEADLOCK
拿到死锁报告后,按下面的顺序阅读,比从头逐行看更快。
1. 找到TRANSACTION 1和TRANSACTION 2
先记录两个事务的 SQL、事务活跃时间和已修改记录数量,确认它们来自哪个接口、定时任务或消息消费者。
2. 查看WAITING FOR THIS LOCK TO BE GRANTED
重点记录表名、索引名、锁模式和记录信息。若报告中出现的索引与预期不同,可能是优化器选择了另一条访问路径,也可能是查询缺少合适索引。
3. 查看HOLDS THE LOCK
把每个事务“已经持有的锁”和“正在等待的锁”连起来。若 A 持有 X 等待 Y,B 持有 Y 等待 X,闭环就很清楚了。
4. 查看被回滚的事务
不要把被回滚的一方简单理解为“有问题的一方”。InnoDB 选择牺牲事务是为了以较低成本解除死锁,真正的修复通常需要同时检查所有参与事务。
七、从事务顺序上修复死锁
1. 所有业务按相同顺序锁定记录
转账、库存扣减、批量状态更新等操作,如果一次需要锁多条记录,应先对主键排序,再按统一顺序更新。
先锁较小ID,再锁较大ID |
无论请求方向如何,都遵循同一顺序,能够直接消除大量交叉等待。
2. 缩短事务范围
事务中不要夹杂 HTTP 请求、文件处理、消息发送或用户交互。先在事务外完成不需要数据库锁的工作,再开启事务执行必要的读写,完成后立即提交或回滚。
连接池中的 autocommit 设置也要检查。异常分支如果没有正确结束事务,连接归还连接池后可能继续持锁,造成看似随机的阻塞。
3. 减小批处理规模
一次更新数万行会持有更多锁,也更容易与其他任务交叉。可以按主键范围分批处理,每批及时提交,同时确保各个任务采用一致的扫描方向。
八、从索引和SQL上减少锁范围
InnoDB 的行锁建立在索引记录上。更新条件缺少合适索引时,数据库可能扫描并锁定比预期更多的记录,扩大与其他事务冲突的范围。
先使用 EXPLAIN 检查执行计划:
EXPLAIN UPDATE orders SET status = 'closed' WHERE user_id = 1001 AND status = 'pending'; |
重点关注实际使用的索引、预计扫描行数和过滤条件。对于这个查询,是否需要 (user_id, status) 之类的联合索引,应结合字段选择性、更新频率和完整工作负载验证,不能只根据一条 SQL 机械添加。
优化时注意以下问题:
WHERE条件未命中索引,导致扫描范围过大;- 联合索引字段顺序与查询条件不匹配;
- 两个业务流程使用不同索引访问同一批数据;
- 范围更新与单行更新同时发生;
SELECT ... FOR UPDATE锁定了实际不需要修改的记录;- 批处理没有固定排序,多个任务反向扫描。
索引能减少被扫描和锁定的记录,但不能保证永远没有死锁。即使两个事务都通过主键更新两行,只要加锁顺序相反,仍然可能形成死锁。
九、应用必须实现有限次数的事务重试
死锁是并发数据库中允许出现的运行时事件。即使已经优化 SQL 和事务顺序,应用仍应对 1213 或相应的 SQLSTATE 40001 做有限次数重试。
正确的重试单位是整个事务,而不是只重放最后失败的那条 SQL:
开始事务 执行全部业务SQL 提交 如果发生死锁:回滚整个事务,短暂等待后重新开始 |
建议设置两到三次上限,并加入小幅随机退避,避免多个请求同时立即重试,再次撞在一起。涉及扣款、发券、创建订单等操作时,还要配合幂等键,避免网络重试与事务重试造成重复业务结果。
不要无限重试。重试次数耗尽后,应记录事务标识、接口、SQL 摘要和错误码,交给监控告警处理。
十、常见错误处理方式
调大innodb_lock_wait_timeout
它只影响普通锁等待时间,不会消除已经形成的死锁循环。盲目调大还可能让阻塞请求积压更久。
发生死锁就重启MySQL
InnoDB 通常已经自动解除该次死锁。重启会中断正常连接和事务,却不会修复相反的加锁顺序、缺失索引或超长事务。
只重试失败SQL
死锁牺牲者的事务已经被回滚。只重放最后一条语句会丢失前面原本属于同一事务的操作,可能造成业务数据不完整。
看到阻塞线程就直接KILL
当前阻塞者不一定是根因,甚至可能是正常执行的重要事务。应先确认事务持续时间、SQL、调用方和影响范围。死锁发生后等待环通常已经解除,再去终止连接往往没有意义。
总结
排查 MySQL 死锁,可以按照下面的顺序进行:
- 根据错误码区分 1213 死锁和 1205 锁等待超时;
- 立即保存
SHOW ENGINE INNODB STATUS中的最近死锁报告; - 必要时开启
innodb_print_all_deadlocks收集完整样本; - 使用 Performance Schema 查看当前锁等待链;
- 从死锁报告中还原每个事务持有什么锁、又在等待什么锁;
- 统一加锁顺序、缩短事务、减小批次,并通过索引减少扫描范围;
- 在应用中对完整事务实施有限次数、带退避的重试。
死锁日志告诉你“哪两个事务撞在了一起”,事务设计和执行计划才能解释“为什么会撞”。把证据采集、SQL 优化和应用重试结合起来,才能既降低死锁频率,也让偶发死锁不再直接变成用户可见的故障。
参考资料
- MySQL 8.4 Reference Manual:Deadlocks in InnoDB
- MySQL 8.4 Reference Manual:How to Minimize and Handle Deadlocks
- MySQL 8.4 Reference Manual:The INFORMATION_SCHEMA INNODB_TRX Table
- MySQL 8.4 Reference Manual:The data_locks Table
- MySQL 8.4 Reference Manual:The data_lock_waits Table




