在 MySQL 的面试中,幻读(Phantom Read)是一个高频考点,而间隙锁(Gap Lock)与临键锁(Next-Key Lock)则是解决幻读问题的核心机制。很多候选人对这两个概念一知半解,被追问“它们到底怎么配合工作”“什么场景下间隙锁会失效”时便语焉不详。本文将从幻读的本质出发,逐层拆解间隙锁与临键锁的原理、加锁规则及其在可重复读隔离级别下如何协同解决幻读问题。
一、先搞清楚:什么是幻读
幻读是指同一个事务在两次相同条件的查询之间,由于其他事务插入或删除了符合该条件的记录,导致第二次查询返回了不同的行集合。注意,幻读强调的是“行数变了”,而不是“某一行的值变了”——后者属于不可重复读。
举个例子:
-- 事务 A
BEGIN;
SELECT * FROM users WHERE age > 20; -- 返回 3 行
-- 此时事务 B 插入一条 age = 25 的记录并提交
SELECT * FROM users WHERE age > 20; -- 返回 4 行,出现幻读
COMMIT;
在可重复读(RR)隔离级别下,MySQL 通过 MVCC 解决了大部分不可重复读问题,但快照读和当前读对幻读的处理方式不同。间隙锁和临键锁主要作用于当前读场景(如 SELECT ... FOR UPDATE、UPDATE、DELETE),用来阻止其他事务在查询范围内插入新行。
二、三种锁的概念辨析
在深入之前,必须先厘清 InnoDB 在 RR 级别下的三种行锁形式:
- 记录锁(Record Lock):锁定索引上的某一条具体记录。
- 间隙锁(Gap Lock):锁定索引记录之间的“间隙”,防止其他事务在间隙中插入新记录。间隙锁之间不互斥,多个事务可以同时持有同一个间隙的间隙锁。
- 临键锁(Next-Key Lock):记录锁 + 间隙锁的组合,锁定一个左开右闭区间
(previous_record, current_record]。它是 InnoDB 在 RR 级别下默认的加锁单位。
用一句话概括:临键锁 = 间隙锁 + 记录锁,它既锁住了记录本身,也锁住了记录前面的间隙。
三、间隙锁如何阻止幻读
幻读的根源在于“插入”。如果一个事务在某个范围内执行当前读,另一个事务在该范围内插入了新行,就会产生幻读。间隙锁的作用正是锁住范围边界之间的空隙,让 INSERT 无法落入这个区间。
假设 users 表的 age 字段上有普通索引,当前索引值为 10, 20, 30。执行:
SELECT * FROM users WHERE age = 20 FOR UPDATE;
InnoDB 不仅会锁住 age = 20 这条记录,还会锁住它前后的间隙,即 (10, 20) 和 (20, 30)。此时另一个事务尝试插入 age = 15 或 age = 25 都会被阻塞,直到第一个事务提交。这样就防止了在 age = 20 查询范围内出现新行。
但要注意:间隙锁只存在于可重复读隔离级别。在读已提交(RC)级别下,间隙锁会被自动关闭,因此 RC 无法完全避免幻读。
四、临键锁的加锁规则
临键锁的加锁范围遵循几个关键原则,这些原则在面试中经常被追问:
- 加锁的基本单位是临键锁,区间为左开右闭。
- 查找过程中访问到的对象才会加锁,没访问到的不加锁。
- 唯一索引等值查询:如果命中唯一索引的等值条件,临键锁会退化为记录锁,不再需要间隙锁。
- 唯一索引等值查询未命中:临键锁退化为间隙锁。
- 非唯一索引等值查询:会加临键锁,并且会向后扫描到第一个不满足条件的记录,对该记录加间隙锁。
- 范围查询:会一直扫描到第一个不满足条件的记录为止。
举个经典例子。假设有表 t(id PK, c UNIQUE, d),数据为 (0,0,0), (5,5,5), (10,10,10)。
执行 SELECT * FROM t WHERE id = 5 FOR UPDATE,由于 id 是主键且等值命中,只加记录锁,锁住 id=5 这一行。
执行 SELECT * FROM t WHERE id = 7 FOR UPDATE,id=7 不存在,临键锁退化为间隙锁,锁住 (5, 10) 这个区间,防止插入 id 在 5 到 10 之间的记录。
执行 SELECT * FROM t WHERE c = 5 FOR UPDATE(c 是唯一索引但此处演示非唯一场景),则会锁住 (0, 5] 和 (5, 10),即临键锁加间隙锁的组合。
五、临键锁与间隙锁如何协同解决幻读
把两者的关系串起来看:
- 临键锁是默认形态,它天然包含了间隙锁,所以范围查询和大部分等值查询都会自动获得防插入能力。
- 间隙锁是临键锁在特定条件下的退化形态,当等值查询未命中唯一索引时,只需要间隙锁即可阻止幻读。
- 记录锁是临键锁的另一退化形态,当唯一索引等值命中时,因为唯一性约束本身保证了不会插入重复值,所以不需要间隙锁。
因此,在 RR 级别下,当前读 + 临键锁/间隙锁构成了防止幻读的完整方案。当事务 A 对某个范围加锁后,事务 B 无法在该范围内插入、更新或删除记录,从而保证了事务 A 两次当前读的结果一致。
六、面试常见追问
追问 1:间隙锁会不会导致死锁?
会。两个事务如果以不同顺序获取相同间隙的锁,就可能互相等待。间隙锁之间虽然不互斥,但插入意向锁与间隙锁互斥,容易形成死锁环路。
追问 2:RC 级别下如何解决幻读?
RC 级别下没有间隙锁,只能依靠 MVCC 的快照读来避免大部分幻读。但如果业务中混用当前读,RC 仍然可能出现幻读,需要应用层加锁或使用串行化隔离级别。
追问 3:间隙锁在什么情况下会失效?
当查询条件没有用到索引时,InnoDB 无法定位间隙,会退化为全表扫描并锁住所有记录和间隙,实际上相当于锁表;此外,如果 where 条件中的字段没有索引,间隙锁也无法精确生效。
七、总结
幻读的本质是“范围插入”打破了查询结果的一致性。MySQL 在可重复读隔离级别下,通过临键锁作为默认加锁单位、间隙锁作为退化形态,在当前读场景中锁住了记录之间的空隙,阻止了其他事务的插入操作,从而解决了幻读问题。理解临键锁的加锁规则、退化条件以及它与唯一索引的关系,是回答这类面试题的关键。记住一句话:临键锁管范围,间隙锁管空隙,记录锁管单行,三者配合才能堵住幻读的缺口。
未经允许不得转载:任鹏个人博客 » MySQL 中的间隙锁与临键锁如何解决幻读问题

