【MySQL修炼篇】从 S 锁 / X 锁 到 Next-Key Lock:MySQL 锁机制硬核拆解
MySQL(InnoDB 引擎)的锁机制是面试和生产环境中最常被深挖的知识点之一。
很多同学能背出“间隙锁、Next-Key Lock、幻读”,但一问具体场景、加锁范围、死锁成因,就卡壳了。
这篇我们从最基础的读锁 / 写锁开始,一层一层拆到最核心的 Next-Key Lock、Gap Lock、Record Lock,并结合实际 SQL 语句说明到底锁住了哪些范围。
1. 基础概念:共享锁(S锁)与排他锁(X锁)
| 锁类型 | 英文 | 兼容性 | 加锁时机 | 典型场景 | 是否阻塞读写 |
|---|---|---|---|---|---|
| S 锁 | Shared Lock | 与 S 锁兼容,与 X 锁互斥 | 普通 SELECT … FOR SHARE / LOCK IN SHARE MODE | 读已提交的记录,想防止别人修改 | 阻塞写 |
| X 锁 | Exclusive Lock | 与任何锁都互斥 | INSERT / UPDATE / DELETE / SELECT … FOR UPDATE | 修改记录、删除记录、加行锁的查询 | 阻塞读写 |
一句话总结:
- S 锁:大家都能读,但不能改(读锁)
- X 锁:只有一个人能读能改,其他人全阻塞(写锁)
2. InnoDB 锁的粒度(从粗到细)
InnoDB 支持三种主要锁粒度:
- 表锁(Table Lock)
- 语法:LOCK TABLES t READ / WRITE
- 很少用,基本被行锁取代
- 意向锁(Intention Lock)
- IS 锁(意向共享锁):我要加 S 锁
- IX 锁(意向排他锁):我要加 X 锁
- 作用:表级兼容检查,加速判断是否能加表锁
- 几乎都是自动加的,程序员不用管
- 行锁(Row Lock)—— InnoDB 锁机制的核心
这是我们今天要重点拆解的部分
3. InnoDB 行锁的三种基本类型
| 锁名 | 锁住什么 | 锁范围示意(假设 id 是主键) | 是否防止幻读 | 加锁时机 | 备注 |
|---|---|---|---|---|---|
| Record Lock | 精确的单条记录 | 只锁 id = 5 这一行 | 否 | 唯一索引精确命中时 | 最小的锁 |
| Gap Lock | 记录之间的“间隙” | 锁 (3,5)、(5,8) 两个间隙 | 是 | 非唯一索引范围查询、重复读隔离级别 | 防止幻读关键 |
| Next-Key Lock | Record + Gap | 锁 id=5 这行 + (3,5) + (5,8) | 是 | 范围查询(>、<、>=、<=、between、like ‘abc%’) | 默认锁 |
一句话记忆:
- Record Lock:只锁点(=)
- Gap Lock:只锁缝(间隙)
- Next-Key Lock:点 + 缝一起锁(最常见)
4. 不同隔离级别下锁的行为(重点!)
| 隔离级别 | 默认锁类型(范围查询) | 是否有 Gap Lock | 是否防止幻读 | 常见加锁场景示例(SELECT * FROM t WHERE id > 5 FOR UPDATE) |
|---|---|---|---|---|
| Read Uncommitted | 无锁 | 无 | 否 | 基本不加锁(脏读) |
| Read Committed | Record Lock | 无 | 否 | 只锁命中的记录,间隙不锁(可能幻读) |
| Repeatable Read | Next-Key Lock | 有 | 是 | 锁记录 + 间隙(MySQL 默认隔离级别,防幻读) |
| Serializable | 表级共享锁(读) | — | 是 | 所有读都加锁,性能极差(基本不用) |
结论:
MySQL 默认隔离级别(RR) + 范围查询 + 加写锁(FOR UPDATE) → 几乎总是 Next-Key Lock。
5. 真实 SQL 加锁范围举例(最容易考)
假设表结构:
CREATE TABLE t (
id INT PRIMARY KEY,
key1 INT,
key2 VARCHAR(20),
INDEX idx_key1 (key1),
INDEX idx_key2 (key2)
);
当前数据:
id | key1 | key2
---+------+------
3 | 10 | abc
5 | 20 | bcd
8 | 30 | cde
11 | 30 | def
案例 1:主键精确匹配
SELECT * FROM t WHERE id = 5 FOR UPDATE;
→ 只加 Record Lock,锁住 id=5 这一行
案例 2:主键范围查询
SELECT * FROM t WHERE id > 5 AND id < 10 FOR UPDATE;
→ Next-Key Lock
锁范围:(5,8] 即锁住 id=8 这行 + (5,8) 间隙
案例 3:非唯一索引等值查询
SELECT * FROM t WHERE key1 = 30 FOR UPDATE;
→ Next-Key Lock
因为 key1=30 有两条记录(id=8 和 id=11),会锁:
- id=8 这行 + (5,8) 间隙
- id=11 这行 + (8,11) 间隙
案例 4:非唯一索引范围查询(最经典幻读场景)
SELECT * FROM t WHERE key1 > 15 AND key1 < 25 FOR UPDATE;
→ Next-Key Lock
锁范围:(10,20] + (20,30]
→ 锁住 id=20 这行 + (10,20) 间隙 + (20,30) 间隙
→ 防止别人在 (10,20) 或 (20,30) 插入新记录(防幻读)
案例 5:唯一索引范围查询 + 等值
SELECT * FROM t WHERE id >= 5 AND id <= 8 FOR UPDATE;
→ Next-Key Lock(左闭右闭)
锁:id=5、id=8 两行 + (3,5)、(5,8)、(8,11) 三个间隙
6. 死锁与间隙锁的典型案例
最经典死锁场景(两个事务交叉锁间隙):
事务 A:
UPDATE t SET key2='xxx' WHERE key1=30; -- 锁 (8,11] + id=11
事务 B:
INSERT INTO t (id,key1,key2) VALUES (9,30,'new'); -- 想插入到 (8,11) 间隙,被 A 挡住
事务 A 再执行:
UPDATE t SET key2='yyy' WHERE key1=30; -- 想锁 id=8,被 B 挡住
→ 死锁(A 等 B,B 等 A)
解决思路:
- 降低隔离级别到 RC(但会丢失幻读保护)
- 尽量用主键或唯一索引定位
- 批量操作时排序一致
- 缩短事务范围
7. 总结:MySQL 锁的“口诀”
- RR 隔离级别 + 范围 + FOR UPDATE → 基本都是 Next-Key Lock(锁记录 + 锁间隙)
- 唯一索引精确命中 → 退化为 Record Lock(只锁点)
- 非唯一索引或范围 → Next-Key Lock(锁点 + 锁间隙)
- 间隙锁只在 RR 隔离级别存在,RC 隔离级别没有
- 幻读问题本质:防止在查询范围内的“新增”或“删除”导致前后两次读不一致
- 死锁 80% 与间隙锁有关,优化方向:用主键、缩短事务、顺序访问
如果你正在准备面试或优化线上慢查询/死锁,可以告诉我具体场景(比如“SELECT … FOR UPDATE 导致死锁”或“RR 隔离下还能幻读吗”),我可以继续给你更细的案例和 SQL 演示。