跳到主要内容
约 6 分钟4 张卡

数据库死锁

数据库死锁是事务之间的循环等待。本文负责 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=OFF1205当前语句。
行锁等待超时,innodb_rollback_on_timeout=ON1205整个事务。

关闭 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
1START TRANSACTION; UPDATE deadlock_demo SET n=n+1 WHERE id=1;
2START TRANSACTION; UPDATE deadlock_demo SET n=n+1 WHERE id=2;
3UPDATE deadlock_demo SET n=n+1 WHERE id=2;
4UPDATE deadlock_demo SET n=n+1 WHERE id=1;

默认检测开启时,一个会话收到 1213。另一会话的等待更新得以继续。最后两个会话均执行 ROLLBACK,清理实验状态。受害事务由检测器选择,验证时读取两边实际结果。

与死锁对照,可让 B 单独持有行 1。A 设置 innodb_lock_wait_timeout=1 后更新行 1。等待超时得到 1205,两边最后都显式回滚。

7. 完整事务重试​

有限重试适用于死锁等瞬时并发冲突。每次尝试都重新读取数据并判断业务前置条件。

  1. 结束失败事务,必要时显式 ROLLBACK。
  2. 在事务外按退避与随机抖动等待。
  3. 新建事务,从业务入口重新执行。
  4. 达到次数上限或总 deadline 时停止。

框架调用也需确保每轮创建新事务。只重放最后一条 SQL 会遗漏此前已回滚的修改。此前根据旧数据作出的判断也需要重新计算。

8. 事务外的副作用​

数据库回滚只撤销数据库管理的修改。已经成功的 HTTP 请求和第三方支付仍保留结果。直接重试整个方法,会再次触发这些操作。

远程调用应移出持锁事务。数据库内记录状态和待发送事件,再异步处理。幂等键与状态条件约束重复执行。事务拆分的设计见事务中的远程调用。

9. 死锁预防​

统一资源访问顺序能减少循环等待。例如所有事务都按主键升序更新同一组记录。

  • 合适索引减少扫描与锁定范围。
  • 短事务减少锁的占用时间。
  • 小批量提交缩小单次竞争范围。
  • 同一热点键可使用受控队列串行处理。

索引内部访问仍会产生锁竞争。应用应保留有限重试和幂等处理。

10. 死锁日志与诊断​

SHOW ENGINE INNODB STATUS 保存最近一次死锁信息。innodb_print_all_deadlocks=ON 将每次死锁写入错误日志。data_locks 和 data_lock_waits 展示当前锁及等待关系。

  1. 找到每个事务持有和等待的锁。
  2. 确认索引、记录或间隙范围。
  3. 回到应用代码,还原事务内 SQL 顺序。
  4. 对照执行计划,检查扫描范围。
  5. 用并发用例验证调整后的死锁率和延迟。

最后一条报错 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。实验后两边均显式回滚。

13. 参考资料​