前面我们聊过 InnoDB 行锁、间隙锁和临键锁。锁一旦用不好,死锁就会找上门。死锁不仅是面试高频考点,线上环境也极易踩坑。
一、什么是死锁
死锁指的是两个或多个事务互相持有对方需要的锁,同时又在等待对方释放锁,谁也不肯先放,最终形成循环等待、永久阻塞的局面。InnoDB 会自动检测死锁,选择回滚代价较小的事务来解除死锁;MyISAM 不支持行锁,因此不会产生死锁。
二、死锁产生四个必要条件(缺一不可)
- 互斥条件:锁是排他的,一个资源(行锁)同一时间只能被一个事务持有
- 持有并等待:事务已经拿到一部分锁,同时还去申请别的锁,却不会释放手上已有的锁
- 不可剥夺:锁不能被其他事务强行抢走,只能由持有者主动释放
- 循环等待:形成环形依赖:A 等 B 的锁,B 等 A 的锁
只要破坏任意一个条件,就能避免死锁。工程上最容易落地的就是破坏循环等待:统一加锁顺序。
三、经典死锁例子
假设表 t 的主键 id 有 1、2 两条数据。
事务 A:
update t set name='a' where id=1; -- 持有id=1行锁
update t set name='a' where id=2; -- 等待id=2的锁
事务 B:
update t set name='b' where id=2; -- 持有id=2行锁
update t set name='b' where id=1; -- 等待id=1的锁
A 拿着 1 等 2,B 拿着 2 等 1,形成循环等待,就是死锁。
复杂场景:间隙锁 + 临键锁混合,是线上死锁最高发的场景,比上面这种简单主键死锁难排查很多。
四、死锁排查完整流程
1. 打开死锁日志(核心)
执行下面的命令查看最近一次死锁信息:
show engine innodb status;
找到 LATEST DETECTED DEADLOCK 这一段,里面包含:
- 两个事务分别执行的 SQL
- 事务 1 持有什么锁、等待什么锁
- 事务 2 持有什么锁、等待什么锁
- 最后回滚了哪一个事务
注意:这里只会保存最近一次死锁,新的死锁会覆盖旧日志。线上建议开启 innodb_print_all_deadlocks,把所有死锁打印到 MySQL error 日志,方便回溯。
2. 读懂死锁日志重点看这几块
TRANSACTION:事务编号、执行 SQL
HOLDS THE LOCK:当前事务已经持有的锁
WAITING FOR THIS LOCK TO BE GRANTED:正在等待的锁
- 锁类型:
RECORD(记录锁)、GAP(间隙锁)、NEXT-KEY(临键锁)
3. 拿到日志之后分析思路
- 看两个事务分别锁住了哪些索引、区间
- 梳理锁的等待链路,确认是不是循环等待
- 判断是主键等值死锁,还是间隙锁 / 临键锁带来的区间锁死锁
五、怎么避免死锁(面试必背方案)
- 统一加锁顺序(最有效):所有业务更新多条记录时,永远按 id 从小到大顺序加锁,避免环形等待。
- 事务尽量小、快:事务里面不要放复杂业务逻辑、不要远程调用,尽快提交,缩短锁持有时间。
- 避免范围查询产生间隙锁:RR 隔离级别,范围查询容易加临键锁,锁区间变大,死锁概率上升;可以评估业务是否允许改成 RC 隔离级别(RC 无间隙锁)。
- 尽量一次申请需要的所有资源
- 合理设置超时:
innodb_lock_wait_timeout,锁等待超时直接回滚,不要无限等(注意:这是锁等待超时,不是死锁检测,死锁是 InnoDB 主动检测)
- 业务层重试机制:捕获死锁异常,短暂休眠后重试
区分两个参数:
innodb_deadlock_detect:死锁检测开关,默认开启
innodb_lock_wait_timeout:锁等待超时时间,默认 50s
六、高频面试问答
Q1:死锁和锁等待有什么区别?
A:锁等待是一个事务等另一个事务,对方执行完就释放,是单向等待;死锁则是双向循环等待,谁都释放不了。
Q2:InnoDB 怎么处理死锁?
A:开启死锁检测,发现循环等待后,计算回滚代价,回滚代价更小的事务,释放锁,让另一个事务继续执行。
Q3:RC 隔离级别会不会死锁?
A:会!RC 没有间隙锁,但依然有行记录锁。只要满足四个必要条件,照样死锁,只是死锁场景更少。
Q4:为什么间隙锁更容易引发死锁?
A:间隙锁锁的是区间,不是单条记录,锁范围更大,更容易出现交叉等待。
七、面试口述精简版
死锁是多个事务互相持有对方需要的锁,形成循环等待。产生需要四个条件:互斥、持有并等待、不可剥夺、循环等待。排查主要靠 show engine innodb status 看死锁日志,查看事务 SQL、持有锁、等待锁。避免死锁的最优方案是统一加锁顺序、缩小事务、缩短事务执行时间;同时谨慎使用范围查询,减少间隙锁带来的大范围锁。
|