适用版本:MySQL 8.0.x(本文示例基于 InnoDB 存储引擎)
一、背景
在 MySQL 事务并发场景中,“死锁”几乎是绕不开的话题。
很多开发第一次在日志里看到类似报错时会非常紧张:
Deadlock found when trying to get lock; try restarting transaction
然后脑子里通常会冒出三个问题:
- 死锁是不是数据库坏了?
- 为什么我的 SQL 看起来很简单,也会死锁?
- 遇到死锁到底应该改代码、改索引,还是改事务?
实际上,死锁不是数据库“出故障”的表现,而是并发事务竞争资源时的一种正常现象。真正需要做的,不是幻想“永远不发生死锁”,而是:
- 理解死锁为什么出现;
- 能快速识别死锁链路;
- 从 SQL、索引、事务设计上降低死锁概率;
- 在应用层具备可恢复的重试策略。
这篇文章就围绕死锁的原理、常见场景与排查步骤展开。
二、什么是死锁
死锁可以用一句话概括:
两个或多个事务互相等待对方持有的锁,形成循环依赖,导致谁都无法继续执行。
例如:
- 事务 A 持有记录 1 的锁,等待记录 2;
- 事务 B 持有记录 2 的锁,等待记录 1;
- 两边都不释放,于是形成闭环。
这就叫死锁。
与普通锁等待有什么区别
普通锁等待是“单向等待”:
- 我等你;
- 只要你提交或回滚,我还能继续。
死锁则是“循环等待”:
- 我等你;
- 你也等我;
- 系统如果不主动介入,双方都走不下去。
三、核心机制:InnoDB 如何处理死锁
InnoDB 并不是等事务永远卡死,而是会做死锁检测。
当系统检测到锁等待形成环时,会从参与死锁的事务中选择一个作为“牺牲者”回滚掉,释放其锁资源,让另一个事务继续执行。
为什么只回滚一个事务
因为死锁的本质是必须打破等待环。只要回滚其中一个事务,环就断开了。
选择谁做牺牲者
InnoDB 通常会尽量选择“代价较小”的事务进行回滚,例如受影响行数更少、回滚成本更低的事务。
所以看到死锁报错时,不代表你的事务一定“写错了”,也可能只是它在死锁冲突中被 MySQL 选中回滚。
四、核心机制:死锁为什么会发生
死锁通常离不开以下几个条件:
- 并发事务同时存在;
- 事务持有锁且继续申请新锁;
- 申请顺序不一致;
- 最终形成循环等待。
在业务系统里,最常见的根因往往不是“数据库很复杂”,而是:
- 不同代码路径更新同一批数据,但顺序不同;
- 范围查询或缺索引导致锁范围扩大;
- 长事务持锁过久,增加冲突窗口;
- 批量更新一次锁太多记录。
五、示例:最典型的两行更新死锁
先准备表:
CREATE TABLE account (
id INT PRIMARY KEY,
balance INT NOT NULL
);
INSERT INTO account VALUES (1, 1000), (2, 1000);
事务 A
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 此时持有 id=1 的排他锁
UPDATE account SET balance = balance + 100 WHERE id = 2;
事务 B
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 2;
-- 此时持有 id=2 的排他锁
UPDATE account SET balance = balance + 100 WHERE id = 1;
如果两个事务并发交错执行,就可能出现:
- A 锁住 1,等 2;
- B 锁住 2,等 1;
- 死锁成立。
这个案例揭示了什么
根本问题并不是“更新了两行”,而是:
- 事务 A 的加锁顺序是
1 -> 2; - 事务 B 的加锁顺序是
2 -> 1; - 顺序不一致,最容易形成死锁。
六、示例:范围查询与插入也可能死锁
很多人以为死锁只会发生在 UPDATE 与 UPDATE 之间,其实范围锁、间隙锁、插入操作也可能参与死锁。
例如在 REPEATABLE READ 下,使用当前读:
SELECT * FROM orders WHERE amount BETWEEN 100 AND 200 FOR UPDATE;
这类语句可能会对索引范围加 next-key lock。此时如果另一个事务尝试在该区间插入新记录,或者从另一个方向锁定相关范围,就可能与前一个事务形成复杂等待关系。
启示
- 死锁不只与“同一行”有关;
- 也可能与“同一索引范围”有关;
- 所以死锁排查不能只盯着某一行主键,还要关注索引和范围条件。
七、如何查看死锁信息
在 MySQL 中,排查死锁最常用的入口是:
SHOW ENGINE INNODB STATUS;
在输出里找到:
LATEST DETECTED DEADLOCK
这一段通常会包含:
- 参与死锁的两个事务;
- 每个事务当前执行的 SQL;
- 它们已经持有的锁;
- 它们正在等待的锁;
- MySQL 最终回滚了哪个事务。
你应该重点看什么
1)事务正在执行的 SQL
先定位“到底是哪两条 SQL 打起来了”。
2)等待的是哪类锁
例如:
- record lock
- gap lock
- next-key lock
这能帮助判断是点更新冲突,还是范围锁冲突。
3)锁对应的是哪个索引
如果能看到索引名,就要继续追问:
- 为什么走了这个索引?
- 是不是因为缺少更合适索引,导致锁范围扩大?
4)哪个事务被回滚
这通常是受害者,不一定是问题根源,但能帮助你在应用日志中找到对应请求。
八、结合业务日志进行排查的思路
仅看数据库死锁日志,通常只能知道“当时谁和谁撞上了”,但想要真正落地整改,往往还要结合业务日志一起看。
一个比较实用的排查顺序
第一步:拿到死锁发生时间
先确定数据库日志中的死锁时间点。
第二步:找到对应应用请求
在应用日志或链路追踪中定位:
- 哪些接口在那个时间点执行了相关 SQL;
- 是否有批量任务、定时任务同时运行;
- 是否有重试造成放大。
第三步:确认事务边界
看代码时,不要只看某一条 SQL,要确认:
- 一个事务里到底执行了几条 SQL;
- 中间有没有额外查询、循环、RPC;
- 是否存在“先查后改、再查再改”的模式。
第四步:对比并发路径的锁顺序
这是排查死锁最核心的一步。
很多死锁案例最终都能归结成一句话:
同一批资源,被不同事务按不同顺序加锁。
九、常见死锁场景总结
9.1 更新顺序不一致
这是最经典、最常见的死锁来源。
典型表现
- 一个事务先更新用户表,再更新订单表;
- 另一个事务先更新订单表,再更新用户表。
结果
两个事务如果并发执行,就可能互相等待。
9.2 缺索引导致锁范围过大
如果更新条件没有合适索引,InnoDB 可能扫描并锁定大量记录,导致本来不该冲突的事务也发生竞争。
典型表现
UPDATE orders SET status = 'DONE' WHERE remark = 'vip';
如果 remark 无索引,那么锁冲突面会明显扩大。
9.3 批量更新一次锁太多记录
例如:
UPDATE orders SET status = 'DONE' WHERE create_time < '2026-06-01';
如果一次更新很多行,且事务执行时间较长,就很容易和其他在线事务形成复杂锁等待。
9.4 范围锁与插入并发
在 REPEATABLE READ 下,范围当前读会使用 next-key lock。此时并发插入可能被间隙锁阻塞,复杂情况下也能形成死锁。
9.5 外键相关操作
涉及父子表更新、删除时,如果多个事务交错操作,也可能因为检查约束和相关索引加锁而产生死锁。
十、死锁出现后怎么办
先说结论:
死锁不是“彻底避免型问题”,而是“尽量减少 + 正确恢复型问题”。
10.1 应用层必须接受死锁可能发生
只要有并发事务,就很难保证百分之百没有死锁。因此关键业务要对死锁错误具备处理能力。
10.2 对可重试事务进行重试
如果事务是:
- 短事务;
- 幂等或可安全重试;
- 没有外部副作用;
那么遇到死锁后,应在应用层进行有限次数重试。
10.3 不要无脑无限重试
无限重试可能把数据库打得更忙。通常要设置:
- 最大重试次数;
- 退避时间;
- 日志告警。
十一、常见问题
11.1 死锁和锁等待超时是一回事吗?
不是。
- 死锁:InnoDB 检测到循环等待,会主动回滚一个事务;
- 锁等待超时:只是一直没拿到锁,超过等待时间后报错。
死锁一定是循环依赖;超时不一定。
11.2 出现一次死锁,是不是 SQL 一定有问题?
不一定。
并发系统中,偶发死锁本来就是可能出现的。关键要看:
- 是否频繁出现;
- 是否影响核心链路;
- 是否能通过统一顺序、缩短事务、优化索引明显降低概率。
11.3 为什么按主键更新也会死锁?
因为死锁关注的是“多个锁的获取顺序”,不是“SQL 是否简单”。
哪怕每次都只是按主键更新一行,只要一个事务要更新多行,而不同事务更新顺序不一致,仍然会死锁。
11.4 为什么死锁日志里看不懂锁信息?
很正常。
刚开始看 SHOW ENGINE INNODB STATUS 时,很多人会被索引名、页号、锁模式绕晕。实践上可以先抓住三个重点:
- 谁在等;
- 等哪条 SQL;
- 锁的是哪张表、哪个索引、哪个范围。
先建立主干,再补细节。
11.5 可以通过关闭死锁检测来解决问题吗?
通常不应该把“关闭检测”当成常规解决方案。
真正应该优先处理的,还是:
- 减少冲突;
- 优化 SQL 与索引;
- 缩短事务;
- 统一加锁顺序。
否则只是把“死锁报错”变成“长时间等待”而已。
十二、实践建议
12.1 对同一批资源统一加锁顺序
这是最有效、最经典的死锁治理手段。
例如需要更新多行账户时,统一按 id 从小到大处理,不要不同代码路径各用各的顺序。
12.2 让 SQL 尽量命中索引
索引不只是性能问题,也是并发问题。索引不准,会导致:
- 扫描更多记录;
- 锁更多索引项;
- 冲突面变大;
- 死锁概率上升。
12.3 缩短事务时间
事务越长,持锁越久,碰撞窗口越大。不要把以下操作放进事务中间:
- RPC 调用;
- 复杂计算;
- 大量循环;
- 人工等待。
12.4 控制单次批量操作规模
大批量更新、删除最好分批执行,而不是一个事务吃下所有数据。
12.5 为死锁设计重试机制
对关键事务,建议:
- 捕获死锁错误码;
- 记录重试日志;
- 做指数退避或短暂随机退避;
- 控制最大重试次数。
12.6 定期复盘死锁日志
如果线上出现过死锁,不要只处理当次报错,最好复盘:
- 高频死锁 SQL 是哪几类;
- 是否与某次发版有关;
- 是否与某个新索引或索引缺失有关;
- 是否与定时任务和在线流量叠加有关。
十三、小结
死锁不是 MySQL 的异常“故障模式”,而是并发事务竞争锁资源时的一种正常结果。真正需要掌握的是:
- 原理上:死锁就是循环等待;
- 机制上:InnoDB 会检测死锁并回滚一个事务;
- 排查上:重点看
SHOW ENGINE INNODB STATUS中的LATEST DETECTED DEADLOCK; - 治理上:统一加锁顺序、优化索引、缩短事务、控制批量规模;
- 恢复上:应用层对可重试事务做好有限重试。
如果要把本文压缩成一句最实用的经验,那就是:
死锁排查不要只看“谁被回滚了”,而要找到“为什么这些事务会以这样的顺序去争同一批资源”。
只要抓住“资源、顺序、范围、事务时长”这四个关键词,大多数 MySQL 死锁问题都能逐步拆开并定位清楚。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!