数据库死锁
数据库死锁是事务之间的循环等待。本文负责 MySQL 8.0 InnoDB 的检测、诊断和失败重试。
1. 面试核心
先记住这几条,再读正文- 循环等待。 事务各自持锁并等待对方资源时,形成死锁等待环。卡 · 死锁的形成
- 死锁检测。
innodb_deadlock_detect=ON时,检测到死锁会回滚一个事务。 - 错误与回滚范围。 1213 回滚整个事务,1205 在
innodb_rollback_on_timeout=OFF时只回滚语句。卡 · 错误码与配置 - 关闭检测的影响。
innodb_deadlock_detect=OFF时,InnoDB 依靠锁等待超时解除死锁。卡 · InnoDB 死锁检测 - 外部副作用。 数据库回滚只撤销受其管理的修改,外部调用结果仍需幂等处理。
2. 死锁的形成
事务 A 先锁订单 1,再等待订单 2。事务 B 先锁订单 2,再等待订单 1。双方各自持有对方需要的锁,形成循环等待。
事务 A:持有 order:1 → 等待 order:2
事务 B:持有 order:2 → 等待 order:1
二级索引访问、外键检查和唯一键冲突也会取得锁。排查时要还原完整事务,追踪已持有的索引记录。
3. InnoDB 死锁检测
MySQL 8.0 默认启用 innodb_deadlock_detect。InnoDB 检查等待关系,发现环后选择受害事务回滚。选择倾向于较小事务,大小按增删改的行数衡量。
受害事务此前的修改全部失效。应用收到 1213 后,应从新事务重做业务单元。锁的具体类型见 InnoDB 锁。
4. 锁等待超时与死锁
锁等待超时表示等待超过 innodb_lock_wait_timeout。持锁事务只是执行很慢,也能让等待方超时。死锁则存在循环依赖。
innodb_rollback_on_timeout=OFF 是 MySQL 8.0 默认值。此时超时只回滚当前语句,此前修改仍在事务内。应用可显式 ROLLBACK,再按完整业务单元重试。
延长超时会增加等待预算,同时延长连接占用。它保持等待环原样,无法消除循环依赖。
5. 错误码与配置
| 情况 | 错误码 | 回滚范围 |
|---|---|---|
| 检测到死锁 | 1213 | 整个受害事务。 |
行锁等待超时,innodb_rollback_on_timeout=OFF | 1205 | 当前语句。 |
行锁等待超时,innodb_rollback_on_timeout=ON | 1205 | 整个事务。 |
关闭 innodb_deadlock_detect 后,由等待超时解除阻塞。高并发争抢同一锁时,关闭检测能减少检测开销。代价是等待环需保留到超时,再由应用结束失败事务。
innodb_lock_wait_timeout 控制 InnoDB 行锁等待。元数据锁使用 lock_wait_timeout。网络超时由客户端和相关网络参数控制。
6. 双会话死锁复现
在独立练习库创建两行数据。
CREATE TABLE deadlock_demo (id INT PRIMARY KEY, n INT) ENGINE=InnoDB;
INSERT INTO deadlock_demo VALUES (1,0),(2,0);
按下表顺序交替操作。第三步保持等待,再执行第四步。
| 步骤 | 会话 A | 会话 B |
|---|---|---|
| 1 | START TRANSACTION; UPDATE deadlock_demo SET n=n+1 WHERE id=1; | |
| 2 | START TRANSACTION; UPDATE deadlock_demo SET n=n+1 WHERE id=2; | |
| 3 | UPDATE deadlock_demo SET n=n+1 WHERE id=2; | |
| 4 | UPDATE deadlock_demo SET n=n+1 WHERE id=1; |
默认检测开启时,一个会话收到 1213。另一会话的等待更新得以继续。最后两个会话均执行 ROLLBACK,清理实验状态。受害事务由检测器选择,验证时读取两边实际结果。
与死锁对照,可让 B 单独持有行 1。A 设置 innodb_lock_wait_timeout=1 后更新行 1。等待超时得到 1205,两边最后都显式回滚。
7. 完整事务重试
有限重试适用于死锁等瞬时并发冲突。每次尝试都重新读取数据并判断业务前置条件。
- 结束失败事务,必要时显式 ROLLBACK。
- 在事务外按退避与随机抖动等待。
- 新建事务,从业务入口重新执行。
- 达到次数上限或总 deadline 时停止。
框架调用也需确保每轮创建新事务。只重放最后一条 SQL 会遗漏此前已回滚的修改。此前根据旧数据作出的判断也需要重新计算。
8. 事务外的副作用
数据库回滚只撤销数据库管理的修改。已经成功的 HTTP 请求和第三方支付仍保留结果。直接重试整个方法,会再次触发这些操作。
远程调用应移出持锁事务。数据库内记录状态和待发送事件,再异步处理。幂等键与状态条件约束重复执行。事务拆分的设计见事务中的远程调用。
9. 死锁预防
统一资源访问顺序能减少循环等待。例如所有事务都按主键升序更新同一组记录。
- 合适索引减少扫描与锁定范围。
- 短事务减少锁的占用时间。
- 小批量提交缩小单次竞争范围。
- 同一热点键可使用受控队列串行处理。
索引内部访问仍会产生锁竞争。应用应保留有限重试和幂等处理。
10. 死锁日志与诊断
SHOW ENGINE INNODB STATUS 保存最近一次死锁信息。innodb_print_all_deadlocks=ON 将每次死锁写入错误日志。data_locks 和 data_lock_waits 展示当前锁及等待关系。
- 找到每个事务持有和等待的锁。
- 确认索引、记录或间隙范围。
- 回到应用代码,还原事务内 SQL 顺序。
- 对照执行计划,检查扫描范围。
- 用并发用例验证调整后的死锁率和延迟。
最后一条报错 SQL 只展示当时的请求。此前执行的语句还会影响已持有的锁。
11. 常见问题
11.1 死锁与数据库状态
死锁是允许锁等待时的一种并发结果。数据库检测并回滚受害事务,是恢复执行的机制。死锁频繁时,应检查访问顺序、事务长度和热点。
11.2 悲观锁与循环等待
悲观锁会让事务等待冲突资源。不同事务按相反顺序取得多把锁,仍会形成死锁。统一顺序和缩短持锁时间能减少竞争。
12 · 面试题卡片
12.1 数据库发生死锁后应该如何定位和处理
出现公司:字节跳动
考察重点
- 循环等待、死锁检测和 victim 回滚。
- 死锁与锁等待超时的区别。
- 完整事务重试、幂等和事务外副作用。
参考回答
定位死锁要还原参与事务的持锁与等待关系;处理上打破等待环,并在新事务中有界重试。
用 SHOW ENGINE INNODB STATUS 查看最近死锁,需要完整记录时开启 innodb_print_all_deadlocks=ON。核对全部 SQL 和索引扫描路径,再通过统一访问顺序、缩短事务和缩小扫描范围减少冲突。
收到 1213 时整个事务已经回滚,应重新执行完整业务单元,并使用退避、次数上限和幂等控制重复副作用。
12.2 1213 与 1205 的回滚范围有何区别,能否直接重试最后一条 SQL?
出现公司:暂无已核验的公开来源(本站自测)
考察重点
- 语句回滚与事务回滚
- 新事务与业务前置条件
参考回答
1213 回滚整个事务。
1205 在 innodb_rollback_on_timeout=OFF 时只回滚当前语句。该参数为 ON 时,1205 回滚整个事务。业务单元需整体重试时,应结束原事务后重新开始。仅重放末条 SQL 会遗漏此前判断和已撤销的修改。
12.3 关闭死锁检测或延长锁等待时间,能否避免死锁?
出现公司:暂无已核验的公开来源(本站自测)
考察重点
- 等待环与超时
- 检测开销和恢复代价
相关内容:InnoDB 死锁检测、错误码与配置。
参考回答
关闭检测或延长等待时间都保留了循环依赖。
innodb_deadlock_detect=OFF 后,由锁等待超时解除阻塞。它能减少检测开销,但延后故障恢复。调整应基于热点负载测试,应用仍需结束失败事务。
12.4 如何用两个会话构造死锁,并与普通锁等待超时区分?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:相反加锁顺序、错误码和清理。
相关内容:双会话死锁复现。
参考回答
A 先更新行 1,B 先更新行 2,均保持事务打开。
A 再更新行 2 等待,B 再更新行 1,便形成环。检测开启时,一个事务收到 1213 并整体回滚。若只有 A 等待 B,则可超时返回 1205。实验后两边均显式回滚。