产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
下列方法有助于最大限度地降低死锁:

  • 按同一顺序访问对象。
  • 避免事务中的用户交互。
  • 保持事务简短并在一个批处理中。
  • 使用低隔离级别。
  • 使用绑定连接。

四条件与五条预防是操作系统教材与 MySQL 文档的标准内容,但 DBA 与开发真正需要的是第三层能力:死锁发生后如何从日志还原现场、以及 InnoDB 特有的死锁形态(间隙锁参与的死锁远超”两事务互等行锁”的教科书案例)。本文补全这两层。

InnoDB 死锁的检测与处置

  • 检测:wait-for graph 等待图探测环路,发现即选回滚代价小的事务(undo 量少者)牺牲,另一事务继续——被牺牲方收到 Deadlock found when trying to get lock(errno 1213)
  • 配置:innodb_deadlock_detect=ON 默认开启;另有 innodb_lock_wait_timeout(默认 50s)兜底非死锁的长等待
  • 应用义务:1213 错误必须重试(回滚方重跑整个事务),这是死锁处理的唯一正确姿势;只重试单条语句会破坏事务原子性

现场还原:读懂死锁日志

SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段给出完整现场:

  • 两个(或多个)事务各自持有(HOLDS)与等待(WAITS FOR)的锁:记录锁/间隙锁类型、索引名、主键值
  • 各自正在执行的 SQL
  • 谁被回滚(WE ROLL BACK TRANSACTION)

还原套路:把 HOLDS/WAITS 画成等待图 → 定位交叉点(通常是两个事务以相反顺序锁两行/两区间)→ 对照业务代码找事务边界。

InnoDB 特有死锁形态(超越教科书)

  1. 间隙锁交叉:RR 隔离级下,范围 UPDATE/INSERT 加间隙锁;两事务对重叠间隙加锁再各自插入 → 插入意向锁互等。典型:并发”不存在则插入”(唯一索引下的 insert intention 冲突)
  2. 唯一索引冲突等待:两事务并发 INSERT 同唯一键,后到者等先到者的 S 锁(dup key check),先到者回滚/提交时序不当形成死锁
  3. UPDATE 二级索引顺序差异:两事务更新同批行但走不同索引(一个走主键一个走二级),加锁顺序不一致 → 交叉
  4. 大事务长锁持有:事务内先更新 A 表再慢慢算再更新 B 表,拉长锁持有窗口放大交叉概率

预防清单(原文五条的展开与补充)

  • 固定加锁顺序:跨表/跨行更新按统一顺序(如主键升序、表名字典序);批量更新先排序再执行
  • 事务短小:事务内不做 RPC/IO/用户交互;把计算移到事务外,事务只做”读-改-写”最小段
  • 索引精确:UPDATE/DELETE 走精确索引避免锁扫描放大(无索引条件可能锁全表行)
  • 隔离级别下调:RC 下无间隙锁(除外键/唯一检查),能消解大半间隙锁死锁;binlog 格式 ROW 前提下 RC 安全
  • 重试兜底:1213 与 1205(锁等待超时)统一指数退避重试(3 次内)
  • 大事务拆分:百万行批量更新分批提交,缩短锁持有
  • 监控:information_schema.INNODB_TRX/data_locks 周期采样长事务;死锁计数(SHOW STATUS LIKE 'Innodb_deadlocks')入告警

一个典型 case 复盘

现象:订单支付回调偶发 1213。日志显示事务 A 持订单行锁等库存行锁,事务 B 持库存行锁等订单行锁——下单流程先锁库存后锁订单,回调流程先锁订单后锁库存。修复:统一为”先订单后库存”,并给回调加 1213 重试。复盘价值:**死锁根因几乎总是”两条代码路径加锁顺序不一致”**,grep 两个事务的 UPDATE 顺序比调参数更快定位。

死锁无法绝对避免(检测+回滚是 InnoDB 的兜底设计),工程目标是低频+可自愈:顺序统一与事务短小降频率,1213 重试保自愈,死锁日志与监控让每次发生都可复盘。三层齐备,死锁就从”事故”降级为”日志里的一行记录”。