问题: 请区分InnoDB中的行锁(Record Lock)、间隙锁(Gap Lock)和临键锁(Next-Key Lock)各自的加锁范围与触发场景;在高并发下,间隙锁极易引发死锁,请举例说明一种典型死锁场景及其排查方法。

👇 点击展开/折叠 面试背诵模板(建议先背这个) Record Lock锁定索引记录本身(单行锁),Gap Lock锁定索引记录之间的间隙(范围锁,阻塞插入),Next-Key Lock = Record Lock + Gap Lock,锁定记录及其前/后间隙。触发场景:Record Lock用于等值查询唯一索引;Gap Lock和Next-Key Lock在RR隔离级别下出现,用于范围查询(>、<、BETWEEN)或唯一索引等值查询但记录不存在时。典型死锁场景:事务A执行`SELECT * FROM t WHERE id > 10 FOR UPDATE`加Next-Key Lock,事务B插入id=15被间隙锁阻塞,事务A再插入id=15时发现被事务B占用,形成循环等待。排查方法:`SHOW ENGINE INNODB STATUS`查看LATEST DETECTED DEADLOCK部分,结合`information_schema.INNODB_TRX`和`INNODB_LOCKS`表定位事务和锁信息。核心优化方向:缩小事务粒度、使用RC级别减少间隙锁、为高并发热点字段设计合理索引避免全表扫描引发的全表间隙锁。
---

一、三种锁的加锁范围与触发场景

锁类型 加锁范围 触发场景
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。

死锁形成过程:

  1. 事务A执行:SELECT * FROM t WHERE id > 3 FOR UPDATE;
    • 加锁范围:(3, +∞),锁定id=5、7的行(Record Lock),并锁定间隙(3,5)、(5,7)、(7,+∞)(Gap Lock)。
  2. 事务B执行:INSERT INTO t VALUES (4);
    • 尝试插入id=4,被事务A持有的间隙锁(3,5)阻塞,等待中。
  3. 事务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格式)。