MySQL的间隙锁(GapLock)使用场景
简单来说,间隙锁锁的是两个索引记录之间的“空隙”,为了防止其他事务在这个空隙中插入新数据,从而导致“幻读”。
1. 前置条件:什么时候会有 Gap Lock?
必须同时满足以下两个条件:
- 隔离级别必须是
READ COMMITTED(RC) 以上的级别(通常是在REPEATABLE READ(RR) 默认级别下)。注意:在 RC 级别下,MySQL 是没有 Gap Lock 的,只有 Record Lock。 - 必须是 InnoDB 引擎。
- 必须是针对索引进行的操作。
2. 核心场景:执行什么 SQL 会加 Gap Lock?
Gap Lock 通常不会单独出现,它通常会和 Record Lock(行锁)结合,形成 Next-Key Lock(临键锁)。 但在以下几种典型 SQL 执行时,你会遇到 Gap Lock:
场景一:范围查询 —— 最常见的场景
当你执行一个范围查询(无论命中还是没命中),且查询条件命中了索引。
SQL 示例:
假设表 t 中有一个主键索引 id,现有数据为 1, 5, 10。
发生了什么?
- 这条 SQL 命中了范围,但在 5 和 10 之间没有数据。
- MySQL 会给
(5, 10)这个间隙加上 Gap Lock。 - 后果:其他事务无法插入
id = 6, 7, 8, 9的数据。因为这些数据都在(5, 10)这个被锁住的间隙里。
场景二:等值查询,但记录不存在 —— “锁空隙”
当你查询一个具体的值,但这个值在表中不存在。InnoDB 会对这个值所在的间隙加锁。
SQL 示例:
数据还是 1, 5, 10。
发生了什么?
- 数据库里没有
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')
发生了什么?
- 因为
name是普通索引,Tom有多条记录,InnoDB 无法确定这是否是唯一的一行(或者为了防止后续插入新的Tom导致幻读)。 - 它不仅锁住
name='Tom'对应的主键行(id=1和id=10),还会锁住这些记录之间的间隙,甚至向两边延伸。 - 后果:其他事务不仅不能修改已有的
Tom,甚至不能在两个Tom之间插入一个新的Tom。
3. 图解:它到底锁住了什么?
假设你的表数据如下(以整型主键为例):
如果你执行
SELECT * FROM t WHERE id = 22 FOR UPDATE;(22不存在)- 加锁范围:间隙锁
(20, 30)。 - 影响:你不能插入 21, 22, 23…29。
- 加锁范围:间隙锁
如果你执行
SELECT * FROM t WHERE id > 20 FOR UPDATE;- 加锁范围:Next-Key Lock
(20, 30]加上 Gap Lock(30, +∞)。 - 影响:你不能插入 21-29,也不能插入 31, 32… 甚至不能修改 30 这一行。
- 加锁范围:Next-Key Lock
4. 为什么要这样设计?(面试加分项)
你可能会问:“我查不存在的数据,为什么要锁住旁边的空隙?不让别人插入?”
这就是为了解决 幻读。
如果没有 Gap Lock:(这里的查询使用的是当前读,不是快照读,使用MVCC的快照读的话不会出现这种问题)
- 事务 A 查询
id > 5,发现只有 10。 - 事务 B 插入了
id = 8,并提交。 - 事务 A 再次查询
id > 5,发现多了个 8。(这就叫幻读,数据像变魔术一样多出来了)。
- 事务 A 查询
有了 Gap Lock:
- 事务 A 查询
id > 5,MySQL 锁住了(5, +∞)的间隙。 - 事务 B 想插入
id = 8,发现被 Gap Lock 挡住了,只能等待。 - 事务 A 再次查询,结果集保持不变。
- 事务 A 查询
5. 总结:死锁的高发区
Gap Lock 是死锁的高发区。
经典死锁场景:
- 事务 A:
SELECT * FROM t WHERE id = 5 FOR UPDATE;(假设 5 不存在,锁住了间隙(1, 10)) - 事务 B:
INSERT INTO t VALUES(6);(被阻塞,因为 6 在间隙里) - 事务 A:也想插入某个数据,或者… 通常是因为两个事务都持有一部分间隙锁,然后去争抢对方锁住的间隙,导致死锁。
Gap Lock 是在 RR 隔离级别下,通过锁定索引记录之间的空隙,来防止其他事务插入数据,从而解决幻读问题的机制。它主要发生在范围查询、未命中记录的等值查询以及非唯一索引的等值查询中。