MySQL InnoDB 如何检测死锁、判定死锁、处理死锁
环境:InnoDB引擎,死锁只发生在行锁等待,表锁MyISAM没有死锁检测。
一、核心:死锁检测算法 ------ 等待图(wait‑for graph)
InnoDB 内部维护一张锁等待图:
- 节点:每一个事务
- 边:
事务A → 事务B,代表:A在等待B持有的锁
死锁判定逻辑:
在等待图中,如果存在环(循环回路) ,就判定发生死锁。
A等B,B等C,C等A,形成环路 → 死锁。
两个关键概念区分
- 锁等待 :A等B,无环,只是普通等待,不会报死锁,会等待直到锁释放,或者超过
innodb_lock_wait_timeout(默认50秒)报锁等待超时。 - 死锁 :等待图出现环路,不是超时,是检测算法主动识别出来。
⚠️注意:死锁 ≠ 锁等待超时。超时是等太久拿不到锁;死锁是检测到循环等待,立刻处理。
二、检测时机
每当一个事务要等待某把锁的时候,InnoDB就触发死锁检测:
- 事务执行 update / delete / select for update,申请行锁;
- 锁被别的事务占有,本事务需要进入等待队列;
- 此时触发死锁检测,遍历等待图,检查有没有环路。
死锁检测会消耗CPU,并发极高场景,大量事务等待锁,检测会带来性能损耗。
参数
innodb_deadlock_detect可以关闭死锁检测,关闭后不会识别死锁,死锁事务只会全部等待直到锁等待超时。
三、死锁发生后:InnoDB怎么做(牺牲策略)
检测出死锁环路之后,InnoDB不会让所有事务无限挂起,选择一个代价最小的事务作为牺牲者回滚:
选择标准:undo log回滚量最小的事务,回滚这个事务,打破等待环。
- 被选中回滚的事务:直接回滚,抛出死锁异常;
- 其他参与死锁的事务:继续执行,继续等待锁。
MySQL返回给应用层的异常
Error 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
错误码:1213,SQLSTATE 40001 ,这就是死锁异常。
业务捕获这个异常,标准做法:做重试。
对比锁等待超时异常:
Error 1205: Lock wait timeout exceeded; try restarting transaction错误码1205,是等待锁超时,不是死锁。
四、死锁日志记录
死锁发生时,最近一次死锁信息保存在内存中:
sql
show engine innodb status;
找到 LATEST DETECTED DEADLOCK 块,里面打印:
- 两个事务分别执行什么SQL
- 各自持有什么锁、等待什么锁
- 哪一个事务被选为牺牲者回滚
开启参数 innodb_print_all_deadlocks=ON,死锁信息同时写入MySQL错误日志,防止新死锁覆盖内存里的死锁记录。
注意:
show engine innodb status只保存最近一次死锁,新死锁会覆盖旧的。
五、关键点梳理
-
怎么判断死锁?
InnoDB维护事务锁等待图,当事务申请锁发生等待时,遍历等待图,发现循环等待环路,判定死锁。
-
死锁是超时发现的吗?
❌不是。死锁是主动图检测发现 ;超时是参数
innodb_lock_wait_timeout控制普通锁等待。就算时间很短,只要出现环路立刻判定死锁。 -
发生死锁后,全部事务回滚吗?
❌不会。只回滚代价最小的那一个事务,其余事务继续等待锁。
-
关闭死锁检测会怎样?
innodb_deadlock_detect=off,不再做图检测;出现死锁时所有事务互相卡住,直到锁等待超时抛出1205错误。高并发热点行场景,有些公司会关闭检测,靠业务重试。 -
应用拿到什么异常?
1213 Deadlock found when trying to get lock; try restarting transaction,业务捕获该异常进行重试。
六、简单流程总结
- T1持有锁A,等待锁B;T2持有锁B,等待锁A;
- T2申请锁A时,进入锁等待,触发死锁检测;
- 扫描等待图发现环路,判定死锁;
- 选undo量小的事务回滚;
- 被回滚的客户端收到1213死锁异常;
- 死锁现场写入innodb status日志。
补充:代码层面现象
Java中JDBC遇到死锁,会抛出SQLTransactionRollbackException,错误码1213;
业务代码捕获该异常,做有限次数重试,是标准处理方案。