跳转到内容

临键锁的加锁与退化机制

草稿难度:高级#MySQL#SQL

原文:CSDN(历史文章导入,当前状态为草稿)

InnoDB 的加锁原则(RR 隔离级别):
基本规则:
默认加 Next-Key Lock(临键锁)
然后根据查询条件判断是否退化
退化条件:
1. 唯一索引等值查询且记录存在 → 退化为记录锁
2. 唯一索引等值查询且记录不存在 → 退化为间隙锁
3. 非唯一索引等值查询 → 记录锁 + 间隙锁(特殊组合)
4. 范围查询 → 不退化,保持 Next-Key Lock
Next-Key Lock(临键锁)
= Record Lock(记录锁)+ Gap Lock(间隙锁)
形象理解:
┌─────────────────────────────────────┐
│ Next-Key Lock (10, 20] │
├─────────────────────────────────────┤
│ Gap Lock: (10, 20) ← 间隙部分 │
│ Record Lock: 20 ← 记录部分 │
└─────────────────────────────────────┘
退化:
→ 只保留记录部分 = Record Lock
→ 只保留间隙部分 = Gap Lock

原则 1: 加锁的基本单位是 Next-Key Lock

Section titled “原则 1: 加锁的基本单位是 Next-Key Lock”
InnoDB 在 RR 级别下:
扫描索引时,默认加 Next-Key Lock
├─ 左开右闭区间
├─ 包含记录 + 前一个间隙
└─ 例如:(10, 20]

原则 2: 查询过程中访问到的对象才会加锁

Section titled “原则 2: 查询过程中访问到的对象才会加锁”
只有实际扫描到的索引记录才加锁
示例:
WHERE id > 10 AND id < 30
└─ 只对扫描到的记录加锁
不会锁 id > 30 的记录

优化 1: 索引等值查询,唯一索引命中记录时退化为记录锁

Section titled “优化 1: 索引等值查询,唯一索引命中记录时退化为记录锁”
-- 表结构
CREATE TABLE t (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- 数据:id = 1, 5, 10, 15, 20
-- 查询
SELECT * FROM t WHERE id = 10 FOR UPDATE;

加锁过程:

1. 扫描索引找到 id=10
2. 默认加 Next-Key Lock: (5, 10]
3. 发现是唯一索引等值查询且记录存在
4. 退化为记录锁:只锁 id=10 这条记录
5. 不锁间隙 (5, 10)
最终加锁:
┌─────────────────────┐
│ id=10 ← 记录锁 │
└─────────────────────┘

优化 2: 索引等值查询,向右遍历且最后一个值不满足条件时,退化为间隙锁

Section titled “优化 2: 索引等值查询,向右遍历且最后一个值不满足条件时,退化为间隙锁”
-- 查询不存在的记录
SELECT * FROM t WHERE id = 7 FOR UPDATE;

加锁过程:

1. 扫描索引查找 id=7
2. 找不到,定位到 id=10(第一个大于 7 的记录)
3. 默认加 Next-Key Lock: (5, 10]
4. 发现 id=10 不满足条件(10≠7)
5. 退化为间隙锁: (5, 10)
6. 不锁 id=10 这条记录
最终加锁:
┌─────────────────────┐
│ (5, 10) ← 间隙锁 │
└─────────────────────┘

3.1 规则 1: 唯一索引等值查询,记录存在

Section titled “3.1 规则 1: 唯一索引等值查询,记录存在”
-- 表结构
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE
);
-- 数据:id = 5, 10, 15, 20
SELECT * FROM users WHERE id = 10 FOR UPDATE;

加锁分析:

步骤 1:扫描主键索引
找到 id=10
步骤 2:默认加 Next-Key Lock
(5, 10]
步骤 3:判断退化条件
✓ 唯一索引(主键)
✓ 等值查询(id = 10)
✓ 记录存在
步骤 4:退化为记录锁
只锁 id=10,不锁间隙 (5, 10)
最终加锁:
┌────────────────────────┐
│ id=10 ← X 锁(记录锁)│
└────────────────────────┘
效果:
- 其他事务可以插入 id=6,7,8,9
- 其他事务不能修改 id=10
SELECT * FROM users WHERE email = 'test@example.com' FOR UPDATE;

加锁分析:

步骤 1:扫描唯一索引 email
找到对应记录
步骤 2:唯一索引加记录锁
步骤 3:回表到主键索引
也加记录锁
最终加锁:
┌────────────────────────────────────┐
│ 唯一索引: email='test@...' ← X 锁 │
│ 主键索引: id=10 ← X 锁 │
└────────────────────────────────────┘
注意:两个索引都只加记录锁,不加间隙锁

3.2 规则 2: 唯一索引等值查询,记录不存在

Section titled “3.2 规则 2: 唯一索引等值查询,记录不存在”
-- 查询不存在的 id
SELECT * FROM users WHERE id = 7 FOR UPDATE;

加锁分析:

步骤 1:扫描主键索引查找 id=7
找不到,定位到 id=10(第一个 > 7 的记录)
步骤 2:默认加 Next-Key Lock
(5, 10]
步骤 3:判断退化条件
✓ 唯一索引
✓ 等值查询
✓ 记录不存在
✓ 扫描到 id=10 但不满足条件(10≠7)
步骤 4:退化为间隙锁
去掉记录锁部分,只保留 (5, 10)
最终加锁:
┌────────────────────────┐
│ (5, 10) ← 间隙锁 │
└────────────────────────┘
效果:
- 阻止插入 id=6,7,8,9
- 不阻止修改 id=5 或 id=10
-- 表结构
CREATE TABLE users (
id INT PRIMARY KEY,
age INT,
INDEX idx_age (age)
);
-- 数据:age = 10, 20, 20, 30
-- 查询
SELECT * FROM users WHERE age = 20 FOR UPDATE;

加锁分析:

步骤 1:扫描 idx_age 找到第一个 age=20
当前位置:age=20(第一条)
步骤 2:加 Next-Key Lock
(10, 20]
步骤 3:继续向右扫描
找到第二个 age=20
步骤 4:继续向右扫描
找到 age=30(不满足条件,停止)
步骤 5:对 age=30 加 Next-Key Lock
(20, 30]
步骤 6:优化(向右遍历的最后一个值不满足条件)
(20, 30] 退化为间隙锁 (20, 30)
最终加锁:
┌────────────────────────────────┐
│ (10, 20] ← Next-Key Lock │
│ age=20(第一条)← 记录锁 │
│ (20, 20](实际上没有间隙) │
│ age=20(第二条)← 记录锁 │
│ (20, 30) ← 间隙锁 │
└────────────────────────────────┘
简化表示:
- 锁住所有 age=20 的记录
- 锁住 (10, 20] 的区间
- 锁住 (20, 30) 的间隙
效果:
- 不能修改 age=20 的记录
- 不能在 (10, 30) 之间插入新记录
-- 范围查询
SELECT * FROM users WHERE id > 10 FOR UPDATE;

加锁分析:

步骤 1:从 id=10 开始向右扫描
步骤 2:扫描到的每条记录加 Next-Key Lock
(10, 15]
(15, 20]
(20, +∞)
步骤 3:不退化
因为是范围查询,需要防止幻读
保持 Next-Key Lock
最终加锁:
┌────────────────────────────────┐
│ (10, 15] ← Next-Key Lock │
│ (15, 20] ← Next-Key Lock │
│ (20, +∞) ← Gap Lock + 记录锁 │
└────────────────────────────────┘
效果:
- 锁住所有 id > 10 的记录
- 锁住所有 id > 10 的间隙
- 完全防止幻读

加锁决策流程
开始查询
选择索引
┌────────────────────────────────┐
│ 1. 扫描索引定位记录 │
│ - 找到匹配的记录 │
│ - 或找到第一个不匹配的记录 │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 2. 默认加 Next-Key Lock │
│ - 左开右闭区间 │
│ - (prev, current] │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 3. 判断是否退化 │
└────────────────────────────────┘
├─ 是唯一索引等值查询?
│ ├─ 是 → 记录存在?
│ │ ├─ 是 → 退化为记录锁 ✓
│ │ └─ 否 → 退化为间隙锁 ✓
│ └─ 否 → 继续
├─ 是否向右遍历的最后一个值?
│ ├─ 是 → 不满足条件?
│ │ ├─ 是 → 退化为间隙锁 ✓
│ │ └─ 否 → 保持 Next-Key Lock
│ └─ 否 → 保持 Next-Key Lock
└─ 否 → 保持 Next-Key Lock
加锁完成
-- 表结构
CREATE TABLE t (
id INT PRIMARY KEY
);
-- 数据:id = 5, 10, 15, 20
-- 查询:范围查询
SELECT * FROM t WHERE id >= 10 AND id < 20 FOR UPDATE;

详细加锁过程:

步骤 1:定位起始位置
扫描索引,找到 id=10
步骤 2:加锁 id=10
默认加 Next-Key Lock: (5, 10]
判断:等值条件?否(是 >=)
保持 Next-Key Lock: (5, 10]
步骤 3:向右扫描,找到 id=15
默认加 Next-Key Lock: (10, 15]
判断:满足条件(15 < 20)
保持 Next-Key Lock: (10, 15]
步骤 4:向右扫描,找到 id=20
默认加 Next-Key Lock: (15, 20]
判断:不满足条件(20 ≮ 20)
这是向右遍历的最后一个值且不满足条件
退化为间隙锁: (15, 20)
最终加锁:
┌────────────────────────────────┐
│ (5, 10] ← Next-Key Lock │
│ (10, 15] ← Next-Key Lock │
│ (15, 20) ← Gap Lock │
└────────────────────────────────┘
效果:
- id=10, 15 被锁(不能修改)
- id=20 未被锁(可以修改)
- (5, 20) 间隙被锁(不能插入)

5.1 场景 1: 主键等值查询(存在)

Section titled “5.1 场景 1: 主键等值查询(存在)”
-- 数据:id = 5, 10, 15, 20
SELECT * FROM t WHERE id = 10 FOR UPDATE;

加锁:

初始:Next-Key Lock (5, 10]
退化:记录锁 [10]
最终:
┌───────────┐
│ id=10 ← X │
└───────────┘
允许:
✓ INSERT INTO t VALUES (6) -- 间隙未锁
✓ UPDATE t SET ... WHERE id=5
✗ UPDATE t SET ... WHERE id=10 -- 被锁

5.2 场景 2: 主键等值查询(不存在)

Section titled “5.2 场景 2: 主键等值查询(不存在)”
-- 数据:id = 5, 10, 15, 20
SELECT * FROM t WHERE id = 7 FOR UPDATE;

加锁:

初始:Next-Key Lock (5, 10]
退化:间隙锁 (5, 10)
最终:
┌────────────┐
│ (5, 10) ← X│
└────────────┘
允许:
✓ UPDATE t SET ... WHERE id=5 -- 记录未锁
✓ UPDATE t SET ... WHERE id=10 -- 记录未锁
✗ INSERT INTO t VALUES (7) -- 间隙被锁
-- 数据:age = 10, 20, 20, 30
-- INDEX idx_age (age)
SELECT * FROM t WHERE age = 20 FOR UPDATE;

加锁:

扫描过程:
1. 找到第一个 age=20
加锁:(10, 20]
2. 继续扫描,找到第二个 age=20
两个 age=20 之间没有间隙
加锁:两条记录
3. 继续扫描,找到 age=30(不满足)
加锁:(20, 30)(退化为间隙锁)
最终:
┌────────────────────────┐
│ (10, 20] ← Next-Key │
│ age=20 (第1条) ← X │
│ age=20 (第2条) ← X │
│ (20, 30) ← Gap │
└────────────────────────┘
允许:
✓ UPDATE ... WHERE age=10
✓ UPDATE ... WHERE age=30
✗ UPDATE ... WHERE age=20 -- 被锁
✗ INSERT ... age=15 -- 间隙被锁
✗ INSERT ... age=25 -- 间隙被锁
-- 数据:id = 5, 10, 15, 20
SELECT * FROM t WHERE id > 10 AND id <= 15 FOR UPDATE;

加锁:

扫描过程:
1. 定位到 id=10
向右扫描,不包括 id=10
2. 找到 id=15(满足条件)
加锁:(10, 15]
3. 继续向右扫描,找到 id=20(不满足)
加锁:(15, 20]
但因为 id=20 不满足条件
退化:(15, 20)
最终:
┌────────────────────────┐
│ (10, 15] ← Next-Key │
│ (15, 20) ← Gap │
└────────────────────────┘
允许:
✓ UPDATE ... WHERE id=10
✓ UPDATE ... WHERE id=20
✗ UPDATE ... WHERE id=15 -- 被锁
✗ INSERT ... id=12 -- 间隙被锁
-- 数据:id = 5, 10, 15, 20
SELECT * FROM t WHERE id >= 10 FOR UPDATE;

加锁:

扫描过程:
1. 定位到 id=10
加锁:(5, 10]
2. 向右扫描所有记录
加锁:(10, 15], (15, 20], (20, +∞)
最终:
┌────────────────────────┐
│ (5, 10] ← Next-Key │
│ (10, 15] ← Next-Key │
│ (15, 20] ← Next-Key │
│ (20, +∞) ← Gap + 记录 │
└────────────────────────┘
效果:
- 锁住 id>=10 的所有记录
- 锁住 (5, +∞) 的所有间隙
- 完全防止幻读
场景 查询类型 记录状态 加锁类型 示例
1 唯一索引等值 存在 记录锁 WHERE id=10(存在)
2 唯一索引等值 不存在 间隙锁 WHERE id=7(不存在)
3 非唯一索引等值 - Next-Key + Gap WHERE age=20
4 唯一索引范围 - Next-Key Lock WHERE id>10
5 非唯一索引范围 - Next-Key Lock WHERE age>20