MySQL发生死锁怎么办?死锁日志、事务定位与索引优化教程

MySQL发生死锁怎么办?死锁日志、事务定位与索引优化教程

线上接口突然报出 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 死锁,可以按照下面的顺序进行:

  1. 根据错误码区分 1213 死锁和 1205 锁等待超时;
  2. 立即保存 SHOW ENGINE INNODB STATUS 中的最近死锁报告;
  3. 必要时开启 innodb_print_all_deadlocks 收集完整样本;
  4. 使用 Performance Schema 查看当前锁等待链;
  5. 从死锁报告中还原每个事务持有什么锁、又在等待什么锁;
  6. 统一加锁顺序、缩短事务、减小批次,并通过索引减少扫描范围;
  7. 在应用中对完整事务实施有限次数、带退避的重试。

死锁日志告诉你“哪两个事务撞在了一起”,事务设计和执行计划才能解释“为什么会撞”。把证据采集、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
知识库

Nginx出现400 Request Header Or Cookie Too Large怎么办?Cookie与请求头排查教程

2026-8-20 16:48:27

知识库

一看就懂的服务器术语词典:从新手到入门工程师的全场景解释指南

2025-4-3 12:15:31