在 MySQL InnoDB 中,临键锁 和 间隙锁 都是用于解决幻读(在 RR 隔离级别下)的锁机制,但它们的作用范围不同:
- 间隙锁(Gap Lock) :只锁定索引记录之间的"间隙",不锁定记录本身。
- 临键锁(Next-Key Lock) :是行锁 + 间隙锁 的组合,锁定一个记录及其前面的间隙(左开右闭区间)。
主要区别
| 维度 | 间隙锁 | 临键锁 |
|---|---|---|
| 锁定范围 | 仅间隙(记录之间的空间) | 记录本身 + 该记录前面的间隙 |
| 是否锁定记录 | ❌ 否 | ✅ 是 |
| 典型产生场景 | 查询条件使用范围但命中不存在的记录 | 默认的索引扫描/范围查询(RR隔离级别) |
| 目的 | 防止幻读(阻止其他事务在间隙中插入新记录) | 防止幻读 + 保护已有记录 |
举例说明
假设有一个表 t,id 是主键,现有记录:1, 3, 5 。
(当前事务隔离级别为 REPEATABLE READ)
1. 临键锁示例
sql
-- 事务 A
BEGIN;
SELECT * FROM t WHERE id = 3 FOR UPDATE;
InnoDB 会对 id=3 这条记录加上临键锁 ,锁定的区间为 (1, 3] (即从上一个记录 1 到当前记录 3 的区间,包含 3 本身)。
此时:
- 事务 B 尝试
INSERT INTO t VALUES (2);→ 阻塞 (因为(1,3)间隙被锁) - 事务 B 尝试
INSERT INTO t VALUES (3);→ 阻塞 (记录3被锁) - 事务 B 尝试
INSERT INTO t VALUES (4);→ 不阻塞 (4属于间隙(3,5),未被锁)
2. 间隙锁示例
sql
-- 事务 A
BEGIN;
SELECT * FROM t WHERE id = 2 FOR UPDATE; -- id=2 不存在
因为 id=2 不存在,InnoDB 只对间隙 (1, 3) 加上间隙锁 ,不会锁定任何记录 (记录 1 和 3 本身未被锁)。
此时:
- 事务 B 尝试
INSERT INTO t VALUES (2);→ 阻塞 (在间隙(1,3)中插入) - 事务 B 尝试
INSERT INTO t VALUES (1);→ 不阻塞 (1是已存在记录,且未被锁;但若1不存在,插入1也不阻塞,因为间隙锁范围是(1,3),不包括1) - 事务 B 尝试
INSERT INTO t VALUES (3);→ 不阻塞 (记录3本身没有被锁) - 事务 B 尝试
UPDATE t SET id = 3 WHERE id = 1;→ 可以执行(不影响间隙)
小结
- 临键锁 = 行锁 + 间隙锁,锁定范围更广,用于保护现有记录及防止相邻间隙的幻读。
- 间隙锁 仅锁住"空白区域",不影响已有记录的修改或删除,只阻止在间隙中插入新数据。
在实际使用中,InnoDB 默认采用临键锁;
- 唯一索引
- 等值查询:当命中
唯一索引等值查询且记录存在时,临键锁会退化为行锁;当查询不存在的记录时,则会退化为间隙锁。 - 范围查询: 看下边的问题范围查询时用的是行锁、临键锁、行锁 哪个? 的答案
- 等值查询:当命中
- 非唯一索引
- 等值查询: 当命中
非唯一索引等值查询且记录存在时,还是间隙锁不会退化,原因看下边的问题 当命中非唯一索引等值查询且记录存在时会加什么样的锁;当查询不存在的记录时,则会退化为间隙锁。 - 范围查询:跟唯一索引差不多
- 等值查询: 当命中
问:范围查询时用的是行锁、临键锁、行锁 哪个?
在 MySQL InnoDB 的 RR(可重复读) 隔离级别下,范围查询 默认使用的是 临键锁(Next-Key Lock) 。
它本质上是 "行锁 + 间隙锁" 的组合,既锁定查询范围内的已有记录(行锁),也锁定这些记录之间的间隙(间隙锁),从而防止幻读。
不同情况的具体表现
| 场景 | RR 隔离级别 | RC 隔离级别 |
|---|---|---|
范围查询(如 WHERE id > 10) |
临键锁(锁定索引区间及间隙) | 行锁(仅锁定命中的记录,无间隙锁) |
| 唯一索引等值查询(记录存在) | 行锁(临键锁退化为行锁) | 行锁 |
| 唯一索引等值查询(记录不存在) | 间隙锁 | 无锁(或行锁?RC 下不加间隙锁,不会阻塞插入) |
举例说明(RR 级别)
表 t(id PK) 有记录:1, 3, 5, 7。
sql
-- 事务 A
BEGIN;
SELECT * FROM t WHERE id > 3 FOR UPDATE; -- 范围查询
此时 InnoDB 会加临键锁,锁定的索引范围是:
- 从
id=3的下一条记录5开始,直到正无穷。 - 具体会锁住
(3,5]、(5,7]、(7, +∞)这些区间(包含记录本身和前面的间隙)。
影响:
- 其他事务无法在
id > 3的范围内插入新记录 (如插入4、6、8都会阻塞),也无法修改或删除5、7等已有记录。 - 这保证了在事务 A 结束前,反复执行相同的范围查询,结果集始终不变(防止幻读)。
与 RC 的对比
如果隔离级别是 RC(读已提交):
- 范围查询只加行锁 (仅锁住实际查到的记录
5和7)。 - 不会锁间隙 ,因此其他事务可以在间隙中插入新记录(如插入
4),导致幻读。
总结
- RR 隔离级别 + 范围查询 → 使用 临键锁(行锁+间隙锁)
- RC 隔离级别 + 范围查询 → 只使用 行锁(无间隙锁,无临键锁)
所以,回答你的问题:范围查询在 InnoDB 中(RR 下)用的是 临键锁。
再问:在RR级别,那如果是 表 t(id PK) 有记录:1, 3, 5, 7。id>=3 呢,锁住的范围是什么?
在 RR(可重复读) 隔离级别下,对 id >= 3 的范围查询使用 FOR UPDATE 加锁时,InnoDB 会使用**临键锁(Next-Key Lock)**锁定以下范围:
锁定的具体范围
表中有记录:1, 3, 5, 7 。
WHERE id >= 3 会命中记录 3, 5, 7。
1. 对每个命中记录加临键锁(左开右闭区间)
- 对于
id = 3:临键锁区间为(1, 3] - 对于
id = 5:临键锁区间为(3, 5] - 对于
id = 7:临键锁区间为(5, 7]
2. 还需要锁定最大记录之后的间隙
- 最后一个记录
7之后的正无穷区间:(7, +∞)(也是一个间隙锁)
3. 综合结果
锁定的所有区间为:
(1, 3] ∪ (3, 5] ∪ (5, 7] ∪ (7, +∞) = (1, +∞)
即:所有大于 1 的 id 值(包括记录和间隙)都被锁住。
- 记录被锁 :
id = 3, 5, 7不能由其他事务修改或删除。 - 间隙被锁 :任何在
(1, +∞)范围内的插入操作 (如插入2, 4, 6, 8, 100)都会被阻塞。 - 记录
1不受影响 :其他事务可以正常修改或删除id = 1的记录。
示例验证
| 其他事务的操作 | 是否阻塞 | 原因 |
|---|---|---|
INSERT INTO t VALUES (2); |
✅ 阻塞 | 落在间隙 (1,3) 中 |
INSERT INTO t VALUES (4); |
✅ 阻塞 | 落在间隙 (3,5) 中 |
INSERT INTO t VALUES (6); |
✅ 阻塞 | 落在间隙 (5,7) 中 |
INSERT INTO t VALUES (8); |
✅ 阻塞 | 落在间隙 (7, +∞) 中 |
UPDATE t SET id=... WHERE id=3; |
✅ 阻塞 | 记录 3 被临键锁锁定 |
DELETE FROM t WHERE id=5; |
✅ 阻塞 | 记录 5 被临键锁锁定 |
UPDATE t SET ... WHERE id=1; |
❌ 不阻塞 | 记录 1 未被锁 |
INSERT INTO t VALUES (0); |
❌ 不阻塞 | 落在 (-∞,1] 区间,未锁定 |
总结
id >= 3锁定的范围是id > 1的所有空间(即(1, +∞))。- 相当于从第一个命中记录的前一个间隙开始(不包括前一个记录本身),一直锁到正无穷。
- 这是 InnoDB 在 RR 级别下为了防止幻读所采取的标准行为。
再问:当命中非唯一索引等值查询且记录存在时会加什么样的锁
在 RR(可重复读) 隔离级别下,当命中非唯一索引等值查询且记录存在 时(例如 SELECT ... WHERE name = 'Alice' FOR UPDATE,name 为非唯一索引),InnoDB 会加 临键锁(Next-Key Lock) ,而不会退化为行锁。
加锁的具体规则
1. 对辅助索引加临键锁
- 对于找到的每条满足条件的辅助索引记录,InnoDB 会对其加上临键锁,锁定区间为
(上一个不同索引值, 当前索引值]。 - 由于非唯一索引可能存在多条相同值 的记录,InnoDB 会锁定所有相同值记录及其之间的间隙 ,并且还会锁定从最后一个相同值到下一个不同值之间的间隙(即右边界扩展到下一个不同值)。
- 最终效果:锁住从上一个不同值到下一个不同值之间的整个区间(不包含左边界和右边界记录本身,但包含区间内的所有记录和间隙)。
2. 对聚集索引(主键)加行锁
- 对于每个匹配的辅助索引记录对应的主键记录 ,InnoDB 也会加上行锁(Record Lock),防止其他事务通过主键直接修改或删除这些行。
举例说明
假设有一个表 t:
sql
CREATE TABLE t (
id INT PRIMARY KEY,
name VARCHAR(10),
INDEX idx_name (name)
);
INSERT INTO t VALUES (1, 'Alice'), (2, 'Alice'), (3, 'Bob'), (4, 'Charlie');
RR 隔离级别下,事务 A 执行:
sql
BEGIN;
SELECT * FROM t WHERE name = 'Alice' FOR UPDATE;
加锁情况:
- 辅助索引
idx_name上的值分布:'Alice' (id=1),'Alice' (id=2),'Bob' (id=3),'Charlie' (id=4) - 上一个不同索引值:无(可以视为
-∞) - 当前索引值:
'Alice' - 下一个不同索引值:
'Bob' - InnoDB 会锁定
(-∞, 'Alice']和('Alice', 'Bob']两个临键锁区间?实际上,由于'Alice'有多条,它会锁定从-∞到'Bob'之间的所有间隙和记录,具体是:- 临键锁1:
(-∞, 'Alice'](包含第一个'Alice'及其前面的间隙) - 临键锁2:
('Alice', 'Alice']?不,对于第二条'Alice',其前一条是第一条'Alice',所以临键锁是('Alice', 'Alice'],这个区间实际上只包含第二条'Alice'本身(因为两条相同的值之间没有间隙)。 - 然后还会对
'Alice'到'Bob'之间的间隙加锁:即临键锁('Alice', 'Bob'](包含'Bob'吗?不,临键锁是左开右闭,'Bob'本身属于这个锁,但'Bob'是下一个不同值,是否会被锁?实际上,这个临键锁会锁住'Bob'记录本身,但'Bob'不满足name='Alice',所以这个锁是必要的,以防止幻读?重要 :根据 InnoDB 实现,范围查询会扫描到第一个不满足条件的记录,并对该记录也加临键锁。因此,对于等值查询name='Alice',InnoDB 会定位到第一条'Alice',然后继续扫描直到第一个不等于'Alice'的记录(即'Bob'),并对'Bob'也加上临键锁('Alice', 'Bob']。这样,其他事务无法在('Alice', 'Bob')间隙中插入新记录(如插入'Alice'或'Alice2'),也无法修改或删除'Bob'本身?实际上,'Bob'被锁住后,其他事务不能修改或删除id=3这一行,但这是否合理?这其实是 InnoDB 为了防止幻读的保守策略:因为如果允许修改'Bob'为'Alice',就会导致新'Alice'出现,影响当前查询结果。所以确实会锁住'Bob'。
- 临键锁1:
因此,最终的锁定范围是 从 -∞ 到 'Bob' 的所有间隙和记录 (包含所有 'Alice' 和 'Bob' 记录)。这样,其他事务:
- 无法插入
name='Alice'的新记录(因为间隙被锁) - 无法插入
name值在'Alice'和'Bob'之间的记录(如'Alice2',按字符串排序在'Alice'和'Bob'之间) - 无法修改
'Bob'为其他值,也无法修改'Alice'记录 - 无法删除
'Alice'或'Bob'记录
对比唯一索引等值查询(记录存在):
- 唯一索引不需要锁定下一个不同值,因为唯一性保证了不会有新记录插入相同值,且修改其他值不影响等值查询。所以退化为行锁。
总结
| 索引类型 | 等值查询记录存在 | RR级别下加的锁 |
|---|---|---|
| 唯一索引(含主键) | ✅ | 行锁(退化为 Record Lock) |
| 非唯一索引 | ✅ | 临键锁(Next-Key Lock,锁定一个区间,包含间隙和记录) |
因此,回答你的问题:非唯一索引等值查询且记录存在时,加的是临键锁(并且还会对对应的主键记录加行锁)。