题目

请解释 MySQL InnoDB 中的间隙锁(Gap Lock) 是什么,以及它解决了什么问题。

问题剖析

先讲个小故事

假设你开了一个水果店,货架上有编号为 1、5、10 的三个苹果。

保洁阿姨过来问你:"你家店里有编号 4 的苹果吗?" 你说没有。话音刚落,供应商突然往货架上放了一个编号 4 的苹果。这下好了——你刚从嘴里说出去的话瞬间成了假话。

这个问题在数据库里就叫幻读(Phantom Read)。而间隙锁,就是 MySQL 解决幻读的武器。

一、间隙锁到底锁了什么?

字面上已经很直白了——锁住的是"间隙",而不是"记录"

-- 假设 id 字段有这些值:1, 5, 10, 15

SELECT * FROM product WHERE id BETWEEN 5 AND 10 FOR UPDATE;

对于这条 SQL,InnoDB 不仅锁住 id=5 和 id=10 这两条存在的记录,还会锁住它们之间的"空档"——也就是 (5, 10) 这个区间。即便这个区间里没有实际数据,你也别想往里面插东西。

粗暴理解:

间隙锁锁的不是已经存在的行,而是锁住了"还不存在但可能被插入的行"

二、为什么会冒出"间隙锁"这个东西?

这就不得不提数据库中一个经典的难题——幻读

什么是幻读?用上面的例子说明:

事务 A 事务 B
查询 id 在 5~10 之间的数据,发现 2 条
插入 id=7 的数据并提交
再次查询 id 在 5~10 之间,发现 3 条("多了"一条!)

事务 A 在同一个事务中,两次同样的查询返回了不同数量的行,就像出现了幻觉——这就是幻读。

行级锁(Record Lock)能锁住已经存在的记录,但它管不了"还不存在"的记录插入。于是,间隙锁诞生了。

三、间隙锁的三种"队友"——认清 InnoDB 锁家族

InnoDB 的行级锁实际上分三种:

锁类型 锁住的对象 一句话解释
Record Lock(记录锁) 索引上的某一条记录 "这条记录归我了,谁也别动"
Gap Lock(间隙锁) 索引记录之间的空隙 "这片空档我占了,别想往里塞东西"
Next-Key Lock(临键锁) Record Lock + Gap Lock 的组合 "这条记录和它前面的空隙,我都包了"

用一张图直观感受一下。假设 id 索引有值:1, 5, 10, 15

数据:    1          5          10         15
索引:  ●——————○  ●——————○  ●——————○  ●
        ↑  间隙  ↑  ↑  间隙  ↑  ↑  间隙  ↑
       (-∞,1)   (1,5)     (5,10)    (10,15)
  • Record Lock 锁的是 ●(存在的记录)
  • Gap Lock 锁的是 ○(间隙,即不存在的空间)
  • Next-Key Lock 锁的是 ○+●(一个区间 + 右端点)

有意思的是:Next-Key Lock 才是 InnoDB 的默认行级锁策略,不是单纯的 Record Lock。

四、代码验证——怎么亲眼看到间隙锁?

光说不练确实有点虚。我们来实战一下:

-- 准备数据
CREATE TABLE product (
    id INT PRIMARY KEY,
    name VARCHAR(50)
) ENGINE=InnoDB;

INSERT INTO product VALUES (1, '苹果'), (5, '香蕉'), (10, '橘子'), (15, '葡萄');

会话 A:开启事务,对 id=10 的记录上锁

BEGIN;
SELECT * FROM product WHERE id = 10 FOR UPDATE;
-- 此时会给 id=10 加 Record Lock
-- 同时给 (5,10) 和 (10,15) 加 Gap Lock

会话 B:尝试往间隙里插数据

BEGIN;
INSERT INTO product VALUES (7, '西瓜');   -- ❌ 被阻塞!因为 (5,10) 被间隙锁锁住了
INSERT INTO product VALUES (12, '梨');    -- ❌ 被阻塞!因为 (10,15) 被间隙锁锁住了
INSERT INTO product VALUES (20, '草莓');  -- ✅ 成功!15 之后没有间隙锁

这就是间隙锁的实际效果——不存在的记录也插不进去,因为间隙被你锁住了。

五、间隙锁的两个重要"脾气"

脾气一:兼容性非常"佛系"

多个事务可以在同一个间隙上同时加间隙锁,它们之间不会冲突。间隙锁的敌人是插入操作,不是另一个间隙锁。

-- 事务 A:给 (5,10) 加间隙锁
-- 事务 B:也给 (5,10) 加间隙锁  ← 没问题,不冲突
-- 事务 A 或 B:想插入 id=7    ← 等吧你

脾气二:只在可重复读(RR)级别下生效

间隙锁是 MySQL InnoDB 在 REPEATABLE READ 隔离级别下的产物。如果你把隔离级别降到 READ COMMITTED,间隙锁就退场了,幻读问题也随之而来。

-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 间隙锁生效的场景:
-- 隔离级别 = REPEATABLE READ(InnoDB 默认)
-- 使用了 FOR UPDATE / LOCK IN SHARE MODE 等锁定读

这就是为什么 InnoDB 在 RR 级别下能拍着胸脯说"我能解决幻读"——间隙锁就是它的底气。

六、间隙锁的坑——别被它咬了一口

间隙锁虽好,但用不好也容易踩坑。面试官可能会顺着追问:

坑1:大范围更新会锁住巨大间隙

-- 假设表里有 id=1 和 id=1000000 两条记录
DELETE FROM product WHERE id > 0;
-- 间隙锁会锁住整个 (1, +∞) 范围,其他插入全部堵死

坑2:间隙锁 + 插入意向锁,可能造成死锁

-- 事务 A:锁定间隙 (5,10),想插入 id=6
-- 事务 B:锁定间隙 (5,10),想插入 id=7
-- 两方都持有间隙锁,又都在等对方释放间隙锁才能插入 → 死锁

MySQL 检测到死锁后会自动回滚其中一个事务,但你的业务代码需要处理好这个异常。

七、一张全景总结表

维度 Record Lock Gap Lock Next-Key Lock
锁什么 一条存在的记录 记录之间的间隙 记录 + 它前面的间隙
防什么 脏读、不可重复读 幻读 两者都防
冲突对象 写操作 插入操作 写操作 + 插入操作
唯一索引等值查询时 退化为 Record Lock 可能退化为 Record Lock
生效前提 锁定读 RR 隔离级别 + 锁定读 RR 隔离级别 + 锁定读

八、常见面试追问

Q1:唯一索引上做等值查询,还会有间隙锁吗?

如果查询的值存在,比如 WHERE unique_key = 10,并且 10 存在,Next-Key Lock 会退化为 Record Lock,只锁这一行,不加间隙锁——因为唯一索引保证了不会有第二条 id=10 的记录,不需要锁间隙。

如果查询的值不存在,比如 WHERE unique_key = 7,但表里没有 7,那会退化为 Gap Lock,锁住它应该在的那个区间,防止别人插入。

Q2:间隙锁锁住的范围怎么确定?

用索引做范围查询时,锁定的是扫描到的索引记录及它们之间的空隙。比如 WHERE id BETWEEN 5 AND 10,会锁住 (1,5]、(5,10]、(10,15]——即找到的第一条记录的前一个间隙,到找到的最后一条记录的后一个间隙。

Q3:能不能禁用间隙锁?

能,但不推荐随便这么做:

  • 把隔离级别降到 READ COMMITTED——间隙锁就没了,但幻读就来了。
  • 开启 innodb_locks_unsafe_for_binlog 参数(MySQL 5.7 之前)。

一般来说,如果你用的是 MySQL 主从复制 + RR 级别,老老实实开着间隙锁是最好的选择。

九、一句话总结

记录锁告诉别人"有主的别动",间隙锁告诉别人"没主的也别动"。

记住这句话,再配合上面那张锁家族对比图,面试官就知道你不仅背了八股文,而且是真理解了。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐