05.锁机制与死锁排查
文章目录
问题: 请区分InnoDB中的行锁(Record Lock)、间隙锁(Gap Lock)和临键锁(Next-Key Lock)各自的加锁范围与触发场景;在高并发下,间隙锁极易引发死锁,请举例说明一种典型死锁场景及其排查方法。
👇 点击展开/折叠 面试背诵模板(建议先背这个)
一、三种锁的加锁范围与触发场景
| 锁类型 | 加锁范围 | 触发场景 |
|---|---|---|
| Record Lock(行锁) | 锁定单个索引记录(行) | 等值查询命中唯一索引(如WHERE id=10);RC级别下范围查询也会退化到Record Lock(无间隙锁) |
| Gap Lock(间隙锁) | 锁定索引记录之间的“间隙”(不包含记录本身) | RR级别下:唯一索引等值查询但记录不存在(锁定待插入位置);范围查询中的间隙部分(如id BETWEEN 10 AND 20锁住(10,20)之间的空隙) |
| Next-Key Lock(临键锁) | 锁定一个索引记录 + 其前一个间隙(即左开右闭区间) | RR级别下:范围查询(>, <, BETWEEN)和唯一索引等值查询命中记录时,锁定该记录及其前面的间隙(防止幻读)。例如id > 10会锁定(10, +∞) |
注意:在RC(Read Committed)隔离级别下,InnoDB不会使用Gap Lock和Next-Key Lock,仅使用Record Lock,因此RC级别下无间隙锁阻塞问题,但存在幻读风险(由MVCC部分缓解)。
二、典型死锁场景与形成过程
场景:RR级别下,事务A和事务B并发操作同一范围的数据。
前置条件:表t有唯一索引id,当前数据为id=1, 3, 5, 7。
死锁形成过程:
- 事务A执行:
SELECT * FROM t WHERE id > 3 FOR UPDATE;- 加锁范围:
(3, +∞),锁定id=5、7的行(Record Lock),并锁定间隙(3,5)、(5,7)、(7,+∞)(Gap Lock)。
- 加锁范围:
- 事务B执行:
INSERT INTO t VALUES (4);- 尝试插入id=4,被事务A持有的间隙锁(3,5)阻塞,等待中。
- 事务A尝试执行:
INSERT INTO t VALUES (6);- 插入id=6,但该位置被事务A自己持有的间隙锁(5,7)阻塞?不,事务A自己持有该锁,因此可以插入? 实际上,若事务A插入成功后,事务B仍在等待间隙锁。但更典型是如下变体:
- 变体:事务A再执行
SELECT * FROM t WHERE id = 4 FOR UPDATE;(等值查询不存在记录),会加间隙锁(3,5),但该间隙已被自己的id>3加锁,无冲突;此时事务B的插入请求仍在等待事务A持有的(3,5)间隙锁,形成死锁?死锁真正形成条件是需要循环等待。 - 经典死锁:事务A持有(3,5)的间隙锁(来自
id>3的锁定),事务B持有(5,7)的间隙锁(同样来自另一个范围查询),然后事务A尝试插入到(5,7)(等待B释放),事务B尝试插入到(3,5)(等待A释放),形成循环等待。
简化版死锁示例:
- 事务A:
SELECT * FROM t WHERE id > 3 FOR UPDATE;(持有(3,5)和(5,7)间隙锁) - 事务B:
SELECT * FROM t WHERE id > 5 FOR UPDATE;(持有(5,7)和(7,+∞)间隙锁) - 事务A尝试插入id=6(需要等待事务B持有的(5,7)间隙锁释放)
- 事务B尝试插入id=4(需要等待事务A持有的(3,5)间隙锁释放)
- 此时事务A等待B,B等待A,死锁形成。
三、死锁排查与定位方法
① 查看InnoDB死锁日志
- 执行
SHOW ENGINE INNODB STATUS; - 在输出中定位
LATEST DETECTED DEADLOCK部分,包含:- 发生死锁的事务ID、SQL语句
- 持有的锁和等待的锁(包括锁类型和索引信息)
- 死锁发生的精确时间戳
② 查询锁和事务信息表
SELECT * FROM information_schema.INNODB_TRX;:查看当前所有活跃事务,包括事务状态、开始时间、是否在等待锁。SELECT * FROM performance_schema.data_locks;(MySQL 8.0):查看当前持有的锁和等待的锁详细信息,包括锁类型、锁模式、锁所在的索引和记录。SELECT * FROM information_schema.INNODB_LOCKS;(旧版本):查看锁信息。
③ 分析死锁根因
- 根据死锁日志中的SQL和锁范围,判断是否存在间隙锁冲突。
- 确认是否因索引选择不当导致全表扫描,触发更大范围的间隙锁。
- 检查是否存在长事务或未提交的事务,导致锁持有时间过长。
四、死锁预防与优化策略
| 策略 | 说明 |
|---|---|
| 降低隔离级别 | 将RR改为RC(读已提交),RC下无间隙锁,死锁概率大幅降低,但需接受幻读风险(大多数业务可容忍) |
| 缩短事务粒度 | 减少事务内SQL执行时间,尽快提交释放锁,降低锁冲突窗口 |
| 固定加锁顺序 | 对多个表或多个行进行更新时,按固定顺序加锁(如按主键升序),避免循环等待 |
| 优化索引设计 | 确保UPDATE和DELETE使用索引,避免全表扫描导致的全表间隙锁(Row-level lock escalation) |
| 添加重试机制 | 在应用层捕获死锁异常(Error 1213),自动重试事务(通常重试2-3次可成功) |
五、核心总结与面试亮点
- RR级别下死锁高发:根源是Next-Key Lock(间隙锁)阻塞并发插入,形成循环等待。
- 定位死锁的关键指令:
SHOW ENGINE INNODB STATUS是黄金法则,配合INNODB_TRX和data_locks表精准定位。 - 最有效的生产优化:若无特殊强一致性要求,推荐将隔离级别设为RC + 使用binlog_format=ROW,既减少间隙锁死锁,又保证主从复制一致性(RC下binlog需设为ROW格式)。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 LGQ的博客!