MySQL 锁机制:记录锁、间隙锁与 Next-Key Lock
面试频率:★★★★★
工作频率:★★★★★
学习重点:理解三种锁的区别,以及 MVCC 和锁分别解决什么问题
⚡ 30 秒速记
InnoDB 常见的三种锁:
Record Lock(记录锁)
↓
锁住已有的索引记录
Gap Lock(间隙锁)
↓
锁住索引记录之间的间隙
↓
主要限制其他事务向间隙插入记录
Next-Key Lock(临键锁)
↓
Record Lock + Gap Lock
↓
既锁记录,也锁记录前面的间隙
假设索引中存在:
10 20 30
●-----------●-----------●
三种锁可以这样理解:
Record Lock:
🔒
20
Gap Lock:
10 20
●===========●
(10,20)
Next-Key Lock:
10 20
●===========🔒
(10,20]
最重要的区别:
| 锁 | 锁什么 | 主要作用 |
|---|---|---|
| Record Lock | 已有索引记录 | 控制对已有记录的并发访问和修改 |
| Gap Lock | 索引记录之间的间隙 | 限制其他事务向间隙插入记录 |
| Next-Key Lock | 记录及其前面的间隙 | 同时控制已有记录和范围内的插入 |
记住:
InnoDB 的行级锁建立在索引记录及索引范围之上,不是简单地给 Java 对象或整行数据加一个锁。
🎤 面试回答
面试官:
MySQL InnoDB 有哪些常见的行级锁?它们有什么区别?
可以回答:
InnoDB 常见的行级锁包括 Record Lock、Gap Lock 和 Next-Key Lock。
Record Lock 是记录锁,锁住已有的索引记录;Gap Lock 是间隙锁,锁住索引记录之间的间隙,主要用于限制其他事务在间隙内插入新记录;Next-Key Lock 可以理解为记录锁和间隙锁的组合,典型区间是
(10,20],既包含记录 20,也包含它前面的间隙。在 RR 隔离级别下,普通快照读主要通过 MVCC 保证一致性读取;而
SELECT ... FOR UPDATE等加锁读以及 UPDATE、DELETE 等操作,会根据访问路径和查询条件使用相应的锁,控制并发修改和范围内的插入。具体加什么锁、锁住多大范围,还要结合索引、查询条件、隔离级别和实际执行计划分析。
一、为什么 MySQL 需要锁?
假设存在一条记录:
id = 1
age = 18
事务 A:
BEGIN;
UPDATE user
SET age = 20
WHERE id = 1;
A 还没有提交。
此时事务 B:
BEGIN;
UPDATE user
SET age = 30
WHERE id = 1;
如果没有并发控制,两个事务同时修改同一条记录,就可能产生冲突。
InnoDB 会通过锁协调这些操作。
事务 A
↓
UPDATE id = 1
↓
持有相关记录的排他锁
↓
尚未 COMMIT
事务 B
↓
UPDATE id = 1
↓
需要获取冲突的锁
↓
等待事务 A
当 A 提交或回滚并释放锁后,B 才能继续获取锁并执行。
注意:
等待并不意味着一定能成功。事务 B 也可能因为锁等待超时或死锁检测而失败。
二、Record Lock:记录锁
1. 什么是记录锁?
Record Lock 锁住的是:
已有的索引记录
例如:
UPDATE user
SET age = 20
WHERE id = 10;
假设 id 是主键,并且存在 id = 10:
主键索引 B+ Tree
1
5
10 🔒
20
30
可以理解为:
锁住 id = 10 对应的索引记录
其他事务如果也要修改这条记录:
UPDATE user
SET age = 30
WHERE id = 10;
就可能需要等待。
但如果修改:
UPDATE user
SET age = 30
WHERE id = 20;
在没有其他锁冲突的情况下,可以并发执行。
2. 为什么记录锁有利于并发?
如果每次 UPDATE 都锁整张表:
事务 A 修改 id = 10
↓
整张表都不能修改
↓
事务 B 修改 id = 20 也得等待
并发能力就会很差。
记录锁可以让不同记录上的操作尽可能并发:
事务 A → id = 10 🔒
事务 B → id = 20 🔒
两者通常可以并发
但要注意:
InnoDB 实际锁住哪些索引记录,取决于 SQL 的访问路径。不能仅凭 WHERE 条件就断言只锁最终返回的那几行。
三、为什么只有记录锁还不够?
假设索引中有:
10
20
30
事务 A 执行:
BEGIN;
SELECT *
FROM user
WHERE id BETWEEN 10 AND 20
FOR UPDATE;
查询范围内有:
10
20
如果只锁已有记录:
10 🔒
20 🔒
事务 B 仍然可能尝试:
INSERT INTO user(id)
VALUES (15);
这样就可能在范围内出现新记录:
原来:
10
20
插入后:
10
15
20
因此,在需要保护一个查询范围的场景下,仅锁住已有记录可能不够。
还需要限制:
其他事务往范围内插入新记录
这就引出了间隙锁。
四、Gap Lock:间隙锁
1. 什么是间隙锁?
假设索引:
10 20 30
●-----------●-----------●
其中:
(10,20)
是一个间隙。
Gap Lock 可以理解为:
锁住索引记录之间的间隙,主要用于限制其他事务向这个间隙插入新记录。
例如锁住:
(10,20)
那么其他事务尝试:
INSERT INTO user(id)
VALUES (15);
可能会被阻塞。
10 15 20
●===========X===========●
Gap Lock
这里的关键是:
id = 15
原本并不存在。
因此,间隙锁不是锁住一条已经存在的 id = 15 记录,而是限制在对应位置插入新记录。
2. 间隙锁会锁住已有记录吗?
单独的 Gap Lock 不锁住已有索引记录本身。
例如:
Gap Lock:
(10,20)
它主要限制:
INSERT 11
INSERT 15
INSERT 19
这些插入操作。
不要把它理解成:
间隙锁
=
把10和20两条记录也一起锁住
这是错误的。
3. 间隙锁的主要作用
一句话:
限制其他事务
↓
向指定索引间隙
↓
插入新的记录
它经常出现在需要保护索引范围的加锁操作中。
注意:
间隙锁并不意味着所有范围查询都会加锁。普通快照 SELECT 与
SELECT ... FOR UPDATE的行为不同。
五、Next-Key Lock:临键锁
这是今天最重要的知识点。
1. 什么是 Next-Key Lock?
可以理解为:
Next-Key Lock
=
Record Lock
+
Gap Lock
假设索引:
10
20
30
一个典型的 Next-Key Lock 区间是:
(10,20]
含义:
10 < id <= 20
其中:
(10,20)
表示记录前面的间隙。
20
表示已有的索引记录。
合起来:
10 20
●=====================🔒
(10,20]
2. 为什么是左开右闭?
因为一个典型的 Next-Key Lock:
锁住某条索引记录
+
它前面的间隙
例如:
索引记录:20
前面的间隙:(10,20)
组合:
(10,20]
因此:
左边不包含10
右边包含20
3. Next-Key Lock 能解决什么问题?
它可以同时控制:
已有记录的并发修改
以及:
其他事务往相关间隙插入新记录
例如:
Record Lock
↓
锁住20
Gap Lock
↓
限制向(10,20)插入
Next-Key Lock
↓
同时控制两者
这就是 Next-Key Lock 在范围加锁场景中非常重要的原因。
六、三种锁的区别
假设索引中有:
10 20 30
●-----------●-----------●
Record Lock
锁:
20
图:
10 20 30
●-----------🔒-----------●
作用:
控制已有记录20的并发访问和修改
Gap Lock
锁:
(10,20)
图:
10 20 30
●===========●-----------●
作用:
限制其他事务往10和20之间插入新记录
Next-Key Lock
锁:
(10,20]
图:
10 20 30
●===========🔒-----------●
作用:
锁住记录20
+
限制向(10,20)插入
七、MVCC 和锁是什么关系?
这是你之前学过的 MVCC 和今天锁机制的连接点。
先区分:
快照读
和:
当前读
1. 快照读
例如普通查询:
SELECT *
FROM user
WHERE id = 10;
在 InnoDB 常见的 RC、RR 隔离级别下,普通一致性 SELECT 通常属于快照读。
主要通过:
MVCC
↓
Read View
↓
版本链
↓
判断记录版本是否可见
来完成一致性读取。
它通常不需要像 FOR UPDATE 那样对查询记录加排他锁。
2. 当前读
例如:
SELECT *
FROM user
WHERE id = 10
FOR UPDATE;
这属于加锁读。
它需要读取当前可用的记录版本,并对相关记录或范围加锁。
常见涉及当前读或加锁访问的操作:
SELECT ... FOR UPDATE;
SELECT ... FOR SHARE;
UPDATE ...;
DELETE ...;
其中:
FOR UPDATE
通常请求排他锁。
FOR SHARE
请求共享锁。
注意:
INSERT 也涉及锁机制,但它有插入意向锁、唯一性检查等自己的细节,不能简单理解成“INSERT 一律使用 Next-Key Lock”。
3. MVCC 与锁的分工
InnoDB 并发控制
↓
┌──────────┴──────────┐
↓ ↓
MVCC 锁
↓ ↓
一致性读取 控制并发访问与修改
↓ ↓
Read View Record Lock
Undo版本链 Gap Lock
Next-Key Lock
可以先记:
普通快照读
↓
主要依赖MVCC
当前读 / 修改
↓
涉及锁机制
但不要理解成:
MVCC和锁完全互不相关
它们是 InnoDB 并发控制中相互配合的机制。
八、RR 隔离级别下如何处理幻读?
面试官经常会问:
InnoDB 在 RR 隔离级别下怎么处理幻读?
不要只回答:
MVCC
应该区分两类读取。
1. 快照读
例如:
BEGIN;
SELECT *
FROM user
WHERE age > 20;
在 RR 下,同一事务的普通一致性读取通常复用第一次一致性读取建立的 Read View。
第一次快照读
↓
建立Read View
↓
后续快照读
↓
复用Read View
↓
根据版本可见性读取数据
因此,其他事务后来插入并提交的新记录,通常不会突然出现在该事务后续使用同一快照的查询结果中。
2. 当前读
例如:
SELECT *
FROM user
WHERE age > 20
FOR UPDATE;
当前读需要考虑:
已有记录
+
范围内可能出现的新记录
在 RR 隔离级别下,InnoDB 可以使用 Next-Key Lock 等锁机制控制范围内的并发插入。
当前读
↓
访问相关索引范围
↓
加记录锁 / 间隙锁 / 临键锁
↓
限制冲突修改和插入
注意:
实际锁范围取决于索引、谓词、唯一性和执行计划。不能认为每一条
FOR UPDATE都一定加 Next-Key Lock。
3. 面试总结
RR 下,普通快照读主要通过 MVCC 和 Read View 保持一致性;对于当前读,InnoDB 会根据查询条件和索引访问路径使用记录锁、间隙锁或 Next-Key Lock 等机制,控制并发修改及范围内的插入。
还有一个容易忽略的细节:
如果同一个事务混用快照读和当前读,当前读可能看到快照读看不到的较新已提交记录,因此不能笼统地说“RR 下任何两次 SELECT 的结果都一定完全一样”。
九、为什么有时候只加记录锁?
例如:
SELECT *
FROM user
WHERE id = 10
FOR UPDATE;
假设:
id是主键
id=10存在
使用主键唯一等值定位
在常见的 InnoDB RR 场景下,这种查询通常可以只对匹配的索引记录加记录锁。
主键唯一等值查询
↓
精确定位已有记录
↓
通常只需要记录锁
原因:
id=10
已经由唯一索引保证:
不会再出现第二条id=10
因此,通常不需要为了防止另一条相同主键记录插入而锁住周围整个间隙。
但要注意:
如果查询的唯一键不存在、使用非唯一索引,或者是范围查询,锁行为可能不同。
十、为什么范围查询可能加 Next-Key Lock?
例如:
SELECT *
FROM user
WHERE id BETWEEN 10 AND 20
FOR UPDATE;
假设索引:
5
10
20
30
查询不仅涉及:
已有的10和20
还可能需要控制:
10和20之间插入的新记录
因此,范围加锁可能涉及:
Record Lock
+
Gap Lock
也就是:
Next-Key Lock
但不要直接背成:
WHERE id BETWEEN 10 AND 20
=
一定只锁(10,20]
实际锁范围可能包含边界之外用于确定扫描结束位置的索引记录或间隙,具体要看执行计划和 InnoDB 的锁定行为。
十一、索引和锁范围有什么关系?
这是很重要的工作知识。
假设:
UPDATE user
SET status = 1
WHERE phone = '13800000000';
如果:
phone有合适索引
MySQL 可能通过索引较快定位目标记录。
索引定位
↓
访问较少的索引记录
↓
通常有利于缩小加锁范围
如果:
phone没有合适索引
MySQL 可能需要扫描大量记录。
扫描大量记录
↓
访问和锁定更多索引记录
↓
更容易发生锁等待
注意:
不能简单说“索引失效就变成表锁”。InnoDB 仍然可能使用行级索引记录锁,只是扫描和加锁范围可能大幅扩大。
这也是为什么:
SQL索引优化
不仅关系到:
查询速度
还可能关系到:
事务并发能力
十二、锁什么时候释放?
以普通事务中的 UPDATE 为例:
BEGIN;
UPDATE user
SET age = 20
WHERE id = 10;
事务持有相关排他锁。
随后:
COMMIT;
或:
ROLLBACK;
事务结束后,相关事务锁通常会释放。
所以:
事务A
↓
UPDATE
↓
持有锁
↓
迟迟不提交
↓
事务B等待
如果业务代码长时间不提交:
锁持有时间变长
↓
其他事务等待时间变长
↓
系统并发能力下降
因此工作中要尽量避免:
开启事务
↓
执行SQL
↓
调用很慢的外部接口
↓
长时间等待
↓
最后才提交
长事务不只是影响性能,也可能放大锁等待问题。
🎯 面试追问
Q1:Record Lock、Gap Lock、Next-Key Lock 有什么区别?
答:
Record Lock 锁住已有索引记录;Gap Lock 锁住索引记录之间的间隙,主要限制插入;Next-Key Lock 是两者的组合,既锁记录,也锁记录前面的间隙。
Q2:Next-Key Lock 的区间为什么通常是左开右闭?
例如:
(10,20]
因为它包含:
记录20
以及:
20前面的间隙(10,20)
Q3:Gap Lock 会锁住已有记录吗?
单独的 Gap Lock 不锁住已有索引记录本身。
它主要用于限制其他事务向对应间隙插入记录。
Q4:为什么需要间隙锁?
因为只锁住已有记录,不能阻止其他事务向查询范围插入新记录。
间隙锁可以限制这些插入。
Q5:普通 SELECT 会加 Next-Key Lock 吗?
在 InnoDB 常见的 RC、RR 隔离级别下,普通一致性 SELECT 通常是快照读,主要通过 MVCC 读取数据,不会因为普通查询就自动对扫描范围加 Next-Key Lock。
但:
SELECT ... FOR UPDATE;
属于加锁读,行为不同。
Q6:RR 下如何处理幻读?
答:
快照读主要依赖 MVCC 和 Read View;当前读则通过包括 Next-Key Lock 在内的锁机制控制相关范围的并发插入。具体锁类型和范围取决于索引与访问路径。
Q7:主键等值查询一定加 Next-Key Lock 吗?
不一定。
对于:
WHERE id = 10
FOR UPDATE
如果主键记录存在,并且通过唯一索引精确定位,通常可以只加记录锁。
Q8:范围查询一定只锁 WHERE 指定的范围吗?
不一定。
实际加锁范围与:
索引
扫描路径
边界记录
隔离级别
执行计划
有关。
不能只看 WHERE 条件就精确判断锁区间。
Q9:没有索引会变成表锁吗?
不能这么说。
InnoDB 的行级锁基于索引记录。
没有合适索引时,可能需要扫描并锁住大量记录,造成接近整表被阻塞的效果,但这不等于锁类型直接变成表锁。
Q10:为什么索引优化能减少锁冲突?
因为合适的索引可能让 SQL 更精确地定位目标记录,减少需要扫描和加锁的索引记录数量。
但最终仍要结合实际执行计划判断。
Q11:为什么长事务容易造成锁等待?
因为事务持有的锁可能一直到提交或回滚才释放。
事务持续时间越长,其他事务等待冲突锁的机会和时间就可能越多。
Q12:INSERT 也是当前读吗?
不要简单把 INSERT 等同于 SELECT ... FOR UPDATE。
INSERT 属于数据修改操作,会涉及插入意向锁、唯一性检查等锁机制;它不是普通 MVCC 快照读,但具体加锁行为与 UPDATE、DELETE、加锁 SELECT 并不完全相同。
🔗 知识串联
你之前学过:
事务隔离级别
↓
MVCC
↓
Undo Log
↓
Read View
今天补上:
Record Lock
↓
Gap Lock
↓
Next-Key Lock
现在整个知识体系可以这样理解:
MySQL事务
↓
InnoDB并发控制
↓
┌──────────┴──────────┐
↓ ↓
MVCC 锁
↓ ↓
一致性读取 并发访问控制
↓ ↓
Read View Record Lock
Undo版本链 Gap Lock
Next-Key Lock
↓ ↓
快照读 当前读 / 修改
再结合索引:
B+ Tree
↓
索引定位
↓
访问哪些索引记录
↓
影响可能的加锁范围
↓
影响锁等待和并发性能
⚠️ 易错点
1. Record Lock 不是简单锁住 Java 意义上的一整行
更准确:
锁住索引记录
2. Gap Lock 不是锁住已有记录
它主要限制:
向间隙插入新记录
3. Next-Key Lock 不等于所有查询都会加的锁
实际是否使用,要看:
隔离级别
索引
查询条件
访问路径
4. 普通 SELECT 不等于 SELECT FOR UPDATE
SELECT *
FROM user
WHERE id = 10;
通常是快照读。
SELECT *
FROM user
WHERE id = 10
FOR UPDATE;
是加锁读。
两者的并发控制机制不同。
5. 不要说 RR 完全依靠 MVCC 解决所有幻读
应该区分:
快照读
↓
MVCC
当前读
↓
锁机制
6. 不要说索引失效就一定变成表锁
更准确:
缺少合适索引
↓
可能扫描并锁住更多索引记录
↓
锁冲突风险增加
7. 不要仅凭 WHERE 条件判断精确锁范围
例如:
WHERE id BETWEEN 10 AND 20
不能直接断言:
只锁(10,20]
具体锁范围还取决于实际索引扫描过程。
❓ 自测
-
InnoDB 常见的三种行级锁是什么?
-
Record Lock 锁住的是什么?
-
Gap Lock 锁住的是什么?
-
Next-Key Lock 是什么?
-
为什么 Next-Key Lock 的典型区间是
(10,20]? -
Gap Lock 会锁住已有记录本身吗?
-
为什么只使用 Record Lock 可能不足以保护范围查询?
-
间隙锁主要限制哪种操作?
-
普通 SELECT 和 SELECT FOR UPDATE 有什么区别?
-
快照读主要依赖什么机制?
-
当前读为什么需要锁?
-
RR 下快照读如何保持一致性?
-
RR 下当前读如何控制范围内的并发插入?
-
主键唯一等值查询什么时候通常只需要记录锁?
-
范围查询为什么可能涉及 Next-Key Lock?
-
没有合适索引是否意味着一定使用表锁?
-
为什么索引可能影响锁范围?
-
为什么长事务容易导致锁等待?
-
UPDATE 的相关事务锁通常什么时候释放?
-
请用自己的话解释 MVCC 和锁的分工。