
你的应用是否遇到过这样的"灵异事件":
- 事务 A
UPDATE了一行id = 10的数据。 - 事务 B 只是想
INSERT一行id = 11的新数据 ,却被阻塞了! - 两个事务操作了完全不同的行 ,却莫名其妙地死锁 (Deadlock) 了。
欢迎来到 MySQL InnoDB 在 REPEATABLE READ 隔离级别下的奇妙世界。这些"灵异事件"的背后,并非 Bug,而是一种精妙的设计,其主角就是我们今天要讨论的"幽灵之锁"------间隙锁 (Gap Lock) 和 临键锁 (Next-Key Lock)。
本文将通过清晰的图示和流程,带你深入理解这三种锁,搞清楚它们是什么 、为什么存在 、以及如何避免它们带来的麻烦。
1. "敌人"是谁?------ 必须理解"幻读"
在深入了解锁之前,我们必须先知道它们要解决的"敌人"是谁:幻读 (Phantom Read)。
-
什么是幻读?
在一个事务(T1)内,两次执行相同的范围查询 (例如
SELECT * FROM users WHERE age > 20),第二次查询看到了第一次查询没有看到的、新插入的行。这些"凭空"多出来的行,就像"幻影"一样,破坏了事务的可重复读性。 -
MVCC 的局限:
我们知道 MVCC (多版本并发控制) 通过"快照读"解决了
SELECT时的"不可重复读"问题。但是,MVCC 无法 阻止"当前读" (SELECT ... FOR UPDATE,UPDATE,DELETE) 所面临的幻读。REPEATABLE READ(RR) 隔离级别(MySQL 默认)的核心承诺,就是必须(在一定程度上)防止幻读。为了兑现这个承诺,InnoDB 仅靠 MVCC 是不够的,它必须引入一种新的锁机制,不仅能锁住已存在的行,还能锁住"不存在的行"。这就是 Gap Lock 和 Next-Key Lock 诞生的原因。
2. "三剑客":锁的类型与类比
为了便于理解,我们用"电影院座位"来做类比。
-
Record Lock(记录锁):- 是什么: 最简单的锁,它直接锁定一行具体存在的索引记录。
- 类比: 你买了一张票,锁定了 F 排 7 号 这个具体的座位。别人不能再买 F7。
- 何时使用: 当你的查询是唯一的、精确的 (例如,通过主键
id = 10或唯一索引username = 'alice')进行UPDATE或SELECT ... FOR UPDATE时,InnoDB 会"智能"地将锁降级为 Record Lock。
-
Gap Lock(间隙锁):- 是什么: 它不锁定任何实际的记录,只锁定索引记录之间的"间隙"。
- 类比: 你没有买票,但你告诉管理员,禁止任何人坐在 F 排 6 号和 F 排 7 号之间的"空隙"里(假设你想在那里放爆米花)。
- 核心作用: 防止其他事务在这个"间隙"中
INSERT新的记录。 - 何时使用: 当你的查询条件是一个不存在的值 (如
UPDATE ... WHERE id = 15,而表中只有 10 和 20),或者在READ COMMITTED隔离级别下(RC级别为了提高并发,会禁用 Gap Lock)。
-
Next-Key Lock(临键锁) - RR 级别的"大杀器"- 是什么:
Record Lock + Gap Lock的合体 。它会锁定一个索引记录,以及这个记录之前的那个"左开右闭"的间隙。 - 类比: 你买了一张票,不仅锁定了 F 排 7 号 座位,还同时 锁定了 F6 到 F7 之间的那个空隙。
- 核心作用: 既锁定行(防止
UPDATE/DELETE),又锁定间隙(防止INSERT),从而完美地防止幻读。 - 何时使用: 这是 InnoDB 在
REPEATABLE READ(RR) 级别下,执行范围查询(如>、<、BETWEEN)或扫描非唯一索引时的默认锁类型!
- 是什么:
3. 锁的"狩猎范围":一个直观的例子
假设我们有一个 users 表,age 字段上有一个普通索引 idx_age,表中有 age 为 10, 20, 30 的记录。
InnoDB 的 idx_age 索引(B+树)在逻辑上可以看作划分了以下几个区间:
(-∞, 10]
(10, 20]
(20, 30]
(30, +∞)
在 REPEATABLE READ 级别下,执行不同的"当前读"会发生什么:
示例 1: SELECT * FROM users WHERE age = 15 FOR UPDATE;
- 分析:
age=15是一个不存在的值,它落在了(10, 20]这个区间内。 - 锁定类型:
Gap Lock(间隙锁)。 - 锁定范围:
(10, 20)。 - 后果: 事务 T1 执行此操作后,事务 T2 无法
INSERTage为 11, 12, ..., 19 的任何记录,T2 会被阻塞。
示例 2: SELECT * FROM users WHERE age = 20 FOR UPDATE; (查询非唯一索引)
- 分析:
age=20是一个存在的值。 - 锁定类型:
Next-Key Lock(临键锁)。 - 锁定范围: 锁定
age=20这条记录(Record Lock),并锁定它之前 的间隙(10, 20](Gap Lock)。 - 后果: T2 无法
INSERTage为 11 到 20 的记录,也无法UPDATE或DELETEage=20的记录。
示例 3: SELECT * FROM users WHERE age > 25 FOR UPDATE;
- 分析: 这是一个范围查询。
- 锁定类型:
Next-Key Lock。 - 锁定范围: 优化器会从第一个大于 25 的记录开始扫描,即
age=30的记录。它会锁定这条记录以及它之前的间隙(20, 30]。然后,它会继续向后扫描,锁定(30, +∞)这个"超级"间隙。 - 后果: 任何
age > 20的INSERT操作都将被阻塞。
4. 为什么会死锁?Gap Lock 的"锅"
Gap Lock 之间是互相兼容的。这就是死锁的根源。
- 类比: T1 锁定了
(10, 20)的间隙(不许别人插队)。T2 也可以同时锁定(10, 20)的间隙(它也不许别人插队)。 - 两个事务都成功持有了同一个间隙的 Gap Lock,它们互不阻塞。
经典死锁流程图:
事务 A 事务 B 数据库 (RR 级别, age 索引) 1. SELECT ... WHERE age = 15 FOR UPDATE 2. 成功! (T1 获得 (10, 20) 上的 Gap Lock) 3. SELECT ... WHERE age = 18 FOR UPDATE 4. 成功! (T2 也获得 (10, 20) 上的 Gap Lock) 此刻, T1 和 T2 共享同一个间隙锁, 互不阻塞 5. INSERT INTO users (age) VALUES (16) 插入请求(16)落入 (10, 20) 间隙 T1 必须等待 T2 释放 Gap Lock 6. INSERT INTO users (age) VALUES (17) 插入请求(17)也落入 (10, 20) 间隙 T2 必须等待 T1 释放 Gap Lock 💥 死锁发生! (T1 等 T2, T2 等 T1) 事务 A 事务 B 数据库 (RR 级别, age 索引)
图解: 两个事务都想在同一个"已上锁"的间隙中 INSERT 数据。INSERT 操作需要一种特殊的"插入意向锁",而这种锁与 Gap Lock 是互斥的。因此,T1 等待 T2 释放 Gap Lock,T2 也等待 T1 释放 Gap Lock,循环等待,死锁。
5. 解决方案:如何"驯服"这些锁?
-
【终极方案】使用
READ COMMITTED(RC) 隔离级别:- 在
RC隔离级别下,InnoDB 会禁用 Gap Lock,只使用 Record Lock。 - 这极大 地提高了并发性,并几乎杜绝了 Gap Lock 导致的死锁。
- 代价: 允许"不可重复读"和"幻读"。
- 适用: 绝大多数互联网业务,对并发性的要求 > 对事务内可重复读的要求。
- 如何设置:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 在
-
【精准方案】查询条件命中"唯一索引":
- 在
RR级别下,如果你的WHERE子句是针对主键 或唯一索引 的等值查询 ,InnoDB 会"智能"地将 Next-Key Lock 降级为纯粹的 Record Lock,从而不锁定间隙。 - 例如:
SELECT ... WHERE id = 10 FOR UPDATE;(id 是主键) 只会锁定id=10这一行。 - 启示: 所有的
UPDATE/DELETE和"当前读"操作,尽量走主键或唯一键。
- 在
-
【规避方案】缩小查询范围:
- 避免在
UPDATE或DELETE中使用大范围的WHERE条件,范围越大,锁定的间隙就越多,冲突概率就越大。
- 避免在
6. 总结
| 锁类型 | 锁定目标 | 核心作用 | 在 RR 级别下的行为 |
|---|---|---|---|
| Record Lock | 索引记录 | 锁定已存在的行 | 当查询命中唯一索引的等值条件时使用 |
| Gap Lock | 索引间的间隙 | 阻止 INSERT |
当查询命中不存在的值时使用 |
| Next-Key Lock | 索引记录 + 间隙 | 锁定行 + 阻止 INSERT |
RR 级别的默认锁 (用于范围扫描和非唯一索引) |
Gap Lock 和 Next-Key Lock 是 InnoDB 为实现 REPEATABLE READ 而付出的"并发成本"。深刻理解它们,你才能在面对死锁时不再迷茫,并根据业务场景,在 RC(高并发)和 RR(高一致性)之间做出最合理的架构选择。