数据库死锁排查实战——从报错日志到根因定位的完整方法论

发布时间:2026/6/24 2:22:59

数据库死锁排查实战——从报错日志到根因定位的完整方法论 十五年数据库领域老炮做过 DBA、架构师、技术顾问。不求颠覆只求靠谱。翻过很多文档也踩过很多坑。做 DBA 这些年凌晨被叫醒的次数里有一半是因为死锁。死锁不可怕可怕的是不知道怎么查。这篇文章我把死锁排查的方法论整理出来从看到报错到定位根因每一步都说清楚。一、先分清两个概念锁等待 vs 死锁很多开发同学把锁等待和死锁搞混先统一一下。锁等待就像去银行办业务你在窗口排队前面有个人在办你只能等。他办完了轮到你。只要前面的人不太磨蹭等一会儿就到了。如果等太久你就不耐烦走了——这就是超时报错。死锁是另一回事你拿着 A 号房的钥匙要进 B 号房另一个人拿着 B 号房的钥匙要进 A 号房。谁也进不去谁也不肯放钥匙。就这么僵住了。数据库检测到这种情况会强制让其中一个人把钥匙交出来回滚事务让另一个人先过。怎么快速区分看报错时间。锁等待是等了很久才报错默认 50 秒死锁是几乎立刻报错。锁等待的报错是Lock wait timeout死锁是Deadlock found。锁等待只卡住一个事务死锁会直接回滚其中一个事务的所有操作。二、死锁报错长什么样先认识一下敌人长什么样。MySQL 的死锁报错ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transactionPostgreSQL 的死锁报错ERROR: 40P01: deadlock detected后面会跟着DETAIL说明哪个进程被谁阻塞。Oracle 的死锁报错ORA-00060: deadlock detected while waiting for resource报错只是告诉你出事了真正有用的信息在死锁日志里。死锁日志在哪里看MySQL 执行SHOW ENGINE INNODB STATUS在输出中找到LATEST DETECTED DEADLOCK这一段这是排查死锁的核心信息。PostgreSQL 需要在配置文件中开启log_lock_waits on然后去日志文件里找。Oracle 在警告日志alert log和跟踪文件trace file中查看。我的习惯生产环境我会定期导出 InnoDB Status 的输出存档。InnoDB 只保留最近一次死锁信息不存档的话新的死锁会把旧的覆盖掉。白纸黑字留证据这是排查的基本功。三、MySQL 死锁排查全流程这是本文的重点。掌握了这个流程90% 的 MySQL 死锁都能搞定。3.1 死锁日志长什么样执行SHOW ENGINE INNODB STATUS\G输出很长只需要关注LATEST DETECTED DEADLOCK这一段。一段典型的死锁日志核心信息有四块第一块死锁发生时间。日志最顶部的时间戳告诉你什么时候发生的死锁。第二块事务 1 的信息。包括事务 ID、活跃时长、执行的 SQL 语句、正在等待哪个锁。重点关注WAITING FOR THIS LOCK TO BE GRANTED——它告诉你事务 1 卡在哪里了。第三块事务 2 的信息。同样包含 SQL 语句、等待的锁。另外多了一行HOLDS THE LOCK(S)——它告诉你事务 2 持有着什么锁而这个锁恰恰就是事务 1 在等的。第四块回滚决策。最后一行WE ROLL BACK TRANSACTION (2)——数据库选择回滚了事务 2。3.2 日志解读三步法拿到死锁日志后不要被满屏的英文吓住。我总结了三步按顺序来就行。第一步看两个事务分别在执行什么 SQL。找到日志中两个UPDATE或INSERT/DELETE/SELECT ... FOR UPDATE语句。这告诉你谁在干什么。第二步看两个事务分别在等什么锁、持有什么锁。找到每个事务的WAITING FOR和HOLDS THE LOCK(S)“。这告诉你谁在等谁”。第三步画出等待关系图找根因。把两个事务的持有和等待关系画出来。如果你发现形成了一个环——A 等 BB 也等 A——那就是死锁。根因往往就藏在这个环里为什么两个事务会交叉持有对方需要的锁是更新顺序不统一还是间隙锁冲突还是事务太大举个实际例子事务 1 执行UPDATE orders SET statuspaid WHERE user_id1001事务 2 执行UPDATE orders SET statusshipped WHERE user_id1002。日志显示事务 1 在等idx_user_id上 user_id1002 的排他锁事务 2 在等idx_user_id上 user_id1001 的排他锁。画出关系图一看两个事务交叉更新不同行没有统一的加锁顺序。根因就是这么简单。3.3 辅助排查命令除了死锁日志MySQL 还提供了几个实时排查的视图。查看当前所有锁等待关系查询performance_schema.data_lock_waits视图MySQL 8.0.1把等待事务和阻塞事务的 ID、SQL 语句都关联出来。紧急情况下直接KILL blocking_thread_id杀掉阻塞线程先恢复业务。查看当前所有持有的锁查询performance_schema.data_locks视图能看到每个事务锁住了哪些行、用的是什么类型的锁。查看当前运行的所有事务查询information_schema.innodb_trx表能看到每个事务的开始时间、状态、正在执行的 SQL。这些命令的具体 SQL 我放在文末的速查表里了需要的时候直接查。四、4 种最常见的死锁场景场景 1交叉更新最经典这是最常见也最容易理解的死锁场景。假设有个转账场景事务 A 要从账户 1 转 100 块到账户 2它先扣账户 1 的钱拿到 id1 的锁再加账户 2 的钱需要 id2 的锁。与此同时事务 B 要从账户 2 转 50 块到账户 1它先扣账户 2 的钱拿到 id2 的锁再加账户 1 的钱需要 id1 的锁。结果事务 A 拿着 id1 的锁等 id2事务 B 拿着 id2 的锁等 id1。死锁。解决方法很简单统一更新顺序。约定所有事务都按 id 从小到大的顺序更新。不管是谁转账都先操作 id 小的那个账户再操作 id 大的那个。这样就不会出现交叉持锁的情况了。这是最简单也最有效的死锁预防手段。我在项目里要求开发团队凡是涉及多行更新必须按主键顺序来。这个规矩省了我不少凌晨的电话。场景 2间隙锁冲突Gap Lock这个场景比较隐蔽很多开发同学不知道它的存在。MySQL 在默认的 REPEATABLE READ 隔离级别下除了行锁还有一种叫间隙锁的东西。简单说它不只锁住已有的记录还会锁住记录之间的空隙防止其他事务在这个空隙里插入新数据。举个例子表里有编号 001、003、005 三条记录。事务 A 执行SELECT * FROM orders WHERE order_no 004 FOR UPDATE——004 不存在但 MySQL 会锁住 003 到 005 之间的间隙不让别人在这里插数据。此时事务 B 尝试插入 004被间隙锁阻塞。如果事务 A 也尝试插入其他数据就可能触发新的锁请求形成死锁。怎么解决有三个方案降隔离级别为 READ COMMITTED推荐。RC 级别下没有间隙锁大部分 OLTP 业务用 RC 就够了不需要严格的可重复读。给字段加唯一索引。唯一索引加等值查询时只锁记录不锁间隙。应用层控制。INSERT 之前先检查是否存在避免触发间隙锁。间隙锁是死锁的重灾区尤其在批量插入场景。如果你的线上频繁出现间隙锁相关的死锁优先考虑把隔离级别从 RR 降到 RC。场景 3大事务 批量更新一个事务里更新了 1000 条记录意味着这 1000 行的锁它全拿着一直要到 COMMIT 才释放。期间其他任何想更新这些行的事务都得排队等。如果大事务中途又去碰了别的事务持有的锁死锁就来了。解决方法拆分大事务分批提交。比如原来一个 UPDATE 更新 1000 行改成每次更新 100 行就 COMMIT 一次。分 10 批跑完。每一批持锁时间短冲突概率大大降低。我的经验单个事务持有锁的时间不要超过 1 秒。1 秒听起来短但对数据库来说够执行上千个查询了。如果你的事务需要锁住上千行数据大概率需要拆分。场景 4SELECT … FOR UPDATE 范围过大SELECT ... FOR UPDATE是手动加排他锁的常用方式。但如果你的查询条件太宽泛会一次性锁住大量行。比如表上有 50 条 status‘pending’ 的记录你执行SELECT * FROM orders WHERE status pending FOR UPDATE50 行全锁了。其他事务想更新其中任何一行都得等你。解决方法缩小查询范围精确加锁。只锁你真正需要的那一行——WHERE id 30 FOR UPDATE。如果确实需要从一批记录中取一条来处理用LIMIT 1限制锁范围。FOR UPDATE 是好东西但要慎用。用之前想清楚我真的需要锁这一行吗能不能缩小范围锁的时间能不能更短五、死锁预防的 5 条军规排查死锁是事后救火预防死锁才是正道。这 5 条是我在项目中反复验证过的军规 1事务尽量短。事务里不要掺杂业务逻辑和远程调用。先做完外部处理最后再开事务快速更新提交。事务多持有一秒锁冲突概率就多一分。军规 2按固定顺序访问资源。所有多行更新的事务按主键升序访问。这是一条铁律简单但有效。军规 3合理使用索引。没有索引的 UPDATE/DELETE 会全表扫描加锁锁住的行数远超你的预期。确保 WHERE 条件中的列有索引让数据库精确定位到需要的行。军规 4控制事务大小。单个事务操作的行数建议不超过 100 行。超过就分批提交。军规 5设置合理的锁等待超时。MySQL 默认的innodb_lock_wait_timeout是 50 秒太长了。生产环境建议改成 10-30 秒。另外确保innodb_deadlock_detect保持 ON默认就是 ON让数据库主动检测死锁而不是傻等超时。六、锁相关命令速查表MySQL 常用命令操作命令查看最近一次死锁日志SHOW ENGINE INNODB STATUS\G找 LATEST DETECTED DEADLOCK 段查看当前锁等待SELECT * FROM performance_schema.data_lock_waits查看当前持有的锁SELECT * FROM performance_schema.data_locks查看当前运行的事务SELECT * FROM information_schema.innodb_trx杀掉阻塞线程KILL thread_id查看锁等待超时SHOW VARIABLES LIKE innodb_lock_wait_timeout设置锁等待超时SET GLOBAL innodb_lock_wait_timeout 15PostgreSQL 常用命令操作命令查看未授予的锁SELECT * FROM pg_locks WHERE NOT granted查看阻塞关系关联pg_stat_activity和pg_locks通过 pid 关联 blocked 和 blocking杀掉阻塞进程SELECT pg_terminate_backend(pid)PostgreSQL 的阻塞关系查询比较长核心思路是从pg_locks中找到NOT granted的锁再通过locktype、database、relation等字段反查是谁持有了对应的锁。需要完整 SQL 的可以评论区留言。总结死锁排查就三步看报错 → 看日志 → 画关系图。看懂了日志里的事务、锁、等待关系根因自然就出来了。预防死锁也是三步事务要短、顺序要统一、索引要到位。处理过几十次死锁每次回到这三步问题都能解决。不需要什么高级工具耐心和方法论就够了。下一篇我会讲慢查询优化的全流程从发现慢 SQL 到调优验证把性能排查三部曲收个尾。有问题评论区见。十五年数据库领域老炮。关注我一起把数据库这件事搞明白。

相关新闻