MySQL的间隙锁(GapLock)使用场景

总结摘要
MySQL的间隙锁会在执行什么SQL语句下使用

简单来说,间隙锁锁的是两个索引记录之间的“空隙”,为了防止其他事务在这个空隙中插入新数据,从而导致“幻读”。


1. 前置条件:什么时候会有 Gap Lock?

必须同时满足以下两个条件:

  1. 隔离级别必须是 READ COMMITTED (RC) 以上的级别(通常是在 REPEATABLE READ (RR) 默认级别下)。注意:在 RC 级别下,MySQL 是没有 Gap Lock 的,只有 Record Lock。
  2. 必须是 InnoDB 引擎
  3. 必须是针对索引进行的操作

2. 核心场景:执行什么 SQL 会加 Gap Lock?

Gap Lock 通常不会单独出现,它通常会和 Record Lock(行锁)结合,形成 Next-Key Lock(临键锁)。 但在以下几种典型 SQL 执行时,你会遇到 Gap Lock:

场景一:范围查询 —— 最常见的场景

当你执行一个范围查询(无论命中还是没命中),且查询条件命中了索引。

SQL 示例: 假设表 t 中有一个主键索引 id,现有数据为 1, 5, 10

1
2
3
-- 事务 A
BEGIN;
SELECT * FROM t WHERE id > 5 AND id < 10 FOR UPDATE;

发生了什么?

  • 这条 SQL 命中了范围,但在 5 和 10 之间没有数据。
  • MySQL 会给 (5, 10) 这个间隙加上 Gap Lock。
  • 后果:其他事务无法插入 id = 6, 7, 8, 9 的数据。因为这些数据都在 (5, 10) 这个被锁住的间隙里。

场景二:等值查询,但记录不存在 —— “锁空隙”

当你查询一个具体的值,但这个值在表中不存在。InnoDB 会对这个值所在的间隙加锁。

SQL 示例: 数据还是 1, 5, 10

1
2
3
-- 事务 A
BEGIN;
SELECT * FROM t WHERE id = 7 FOR UPDATE;

发生了什么?

  • 数据库里没有 id=7
  • MySQL 会扫描索引,发现 7 落在了 (5, 10) 这个区间。
  • MySQL 会给 (5, 10) 这个间隙加上 Gap Lock。
  • 后果:其他事务无法插入 id = 6, 7, 8, 9 的数据。直到事务 A 提交。

场景三:命中“非唯一索引”的等值查询

这是面试中最爱考的细节!如果你的查询条件是普通索引(Secondary Index),而不是唯一索引(Primary Key / Unique Key),即使你只查一行,也可能锁住一个间隙。

SQL 示例: 表结构:

  • id (主键)
  • name (普通索引, 有重复值)
  • 现有数据:id(1, name='Tom'), id(5, name='Jerry'), id(10, name='Tom')
1
2
3
-- 事务 A
BEGIN;
SELECT * FROM t WHERE name = 'Tom' FOR UPDATE;

发生了什么?

  • 因为 name 是普通索引,Tom 有多条记录,InnoDB 无法确定这是否是唯一的一行(或者为了防止后续插入新的 Tom 导致幻读)。
  • 它不仅锁住 name='Tom' 对应的主键行(id=1id=10),还会锁住这些记录之间的间隙,甚至向两边延伸。
  • 后果:其他事务不仅不能修改已有的 Tom,甚至不能在两个 Tom 之间插入一个新的 Tom

3. 图解:它到底锁住了什么?

假设你的表数据如下(以整型主键为例):

1
2
3
记录值:  10    20    30
间隙:    |-----|-----|-----
        (-∞,10) (10,20) (20,30) (30,+∞)
  1. 如果你执行 SELECT * FROM t WHERE id = 22 FOR UPDATE; (22不存在)

    • 加锁范围:间隙锁 (20, 30)
    • 影响:你不能插入 21, 22, 23…29。
  2. 如果你执行 SELECT * FROM t WHERE id > 20 FOR UPDATE;

    • 加锁范围:Next-Key Lock (20, 30] 加上 Gap Lock (30, +∞)
    • 影响:你不能插入 21-29,也不能插入 31, 32… 甚至不能修改 30 这一行。

4. 为什么要这样设计?(面试加分项)

你可能会问:“我查不存在的数据,为什么要锁住旁边的空隙?不让别人插入?”

这就是为了解决 幻读

  • 如果没有 Gap Lock:(这里的查询使用的是当前读,不是快照读,使用MVCC的快照读的话不会出现这种问题)

    1. 事务 A 查询 id > 5,发现只有 10。
    2. 事务 B 插入了 id = 8,并提交。
    3. 事务 A 再次查询 id > 5,发现多了个 8。(这就叫幻读,数据像变魔术一样多出来了)。
  • 有了 Gap Lock

    1. 事务 A 查询 id > 5,MySQL 锁住了 (5, +∞) 的间隙。
    2. 事务 B 想插入 id = 8,发现被 Gap Lock 挡住了,只能等待。
    3. 事务 A 再次查询,结果集保持不变。

5. 总结:死锁的高发区

Gap Lock 是死锁的高发区

经典死锁场景:

  1. 事务 A:SELECT * FROM t WHERE id = 5 FOR UPDATE; (假设 5 不存在,锁住了间隙 (1, 10)
  2. 事务 B:INSERT INTO t VALUES(6); (被阻塞,因为 6 在间隙里)
  3. 事务 A:也想插入某个数据,或者… 通常是因为两个事务都持有一部分间隙锁,然后去争抢对方锁住的间隙,导致死锁。

Gap Lock 是在 RR 隔离级别下,通过锁定索引记录之间的空隙,来防止其他事务插入数据,从而解决幻读问题的机制。它主要发生在范围查询未命中记录的等值查询以及非唯一索引的等值查询中。