临键锁的加锁与退化机制
原文:CSDN(历史文章导入,当前状态为草稿)
1.1 简介
Section titled “1.1 简介”InnoDB 的加锁原则(RR 隔离级别):
基本规则:默认加 Next-Key Lock(临键锁)然后根据查询条件判断是否退化
退化条件:1. 唯一索引等值查询且记录存在 → 退化为记录锁2. 唯一索引等值查询且记录不存在 → 退化为间隙锁3. 非唯一索引等值查询 → 记录锁 + 间隙锁(特殊组合)4. 范围查询 → 不退化,保持 Next-Key Lock1.2 三种锁的关系
Section titled “1.2 三种锁的关系”Next-Key Lock(临键锁)= Record Lock(记录锁)+ Gap Lock(间隙锁)
形象理解:┌─────────────────────────────────────┐│ Next-Key Lock (10, 20] │├─────────────────────────────────────┤│ Gap Lock: (10, 20) ← 间隙部分 ││ Record Lock: 20 ← 记录部分 │└─────────────────────────────────────┘
退化:→ 只保留记录部分 = Record Lock→ 只保留间隙部分 = Gap Lock2. 加锁的基本原则
Section titled “2. 加锁的基本原则”2.1 两个核心原则
Section titled “2.1 两个核心原则”原则 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 的记录2.2 两个优化规则
Section titled “2.2 两个优化规则”优化 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=102. 默认加 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=72. 找不到,定位到 id=10(第一个大于 7 的记录)3. 默认加 Next-Key Lock: (5, 10]4. 发现 id=10 不满足条件(10≠7)5. 退化为间隙锁: (5, 10)6. 不锁 id=10 这条记录
最终加锁:┌─────────────────────┐│ (5, 10) ← 间隙锁 │└─────────────────────┘3. 退化规则详解
Section titled “3. 退化规则详解”3.1 规则 1: 唯一索引等值查询,记录存在
Section titled “3.1 规则 1: 唯一索引等值查询,记录存在”-- 表结构CREATE TABLE users ( id INT PRIMARY KEY, email VARCHAR(100) UNIQUE);
-- 数据:id = 5, 10, 15, 20主键查询(记录存在)
Section titled “主键查询(记录存在)”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唯一索引查询(记录存在)
Section titled “唯一索引查询(记录存在)”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: 唯一索引等值查询,记录不存在”主键查询(记录不存在)
Section titled “主键查询(记录不存在)”-- 查询不存在的 idSELECT * 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=103.3 规则 3: 非唯一索引等值查询
Section titled “3.3 规则 3: 非唯一索引等值查询”-- 表结构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) 之间插入新记录3.4 规则 4: 范围查询不退化
Section titled “3.4 规则 4: 范围查询不退化”-- 范围查询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 的间隙- 完全防止幻读4. 完整的加锁流程
Section titled “4. 完整的加锁流程”4.1 加锁流程图
Section titled “4.1 加锁流程图” 加锁决策流程
开始查询 ↓选择索引 ↓┌────────────────────────────────┐│ 1. 扫描索引定位记录 ││ - 找到匹配的记录 ││ - 或找到第一个不匹配的记录 │└────────────────────────────────┘ ↓┌────────────────────────────────┐│ 2. 默认加 Next-Key Lock ││ - 左开右闭区间 ││ - (prev, current] │└────────────────────────────────┘ ↓┌────────────────────────────────┐│ 3. 判断是否退化 │└────────────────────────────────┘ ↓ ├─ 是唯一索引等值查询? │ ├─ 是 → 记录存在? │ │ ├─ 是 → 退化为记录锁 ✓ │ │ └─ 否 → 退化为间隙锁 ✓ │ └─ 否 → 继续 │ ├─ 是否向右遍历的最后一个值? │ ├─ 是 → 不满足条件? │ │ ├─ 是 → 退化为间隙锁 ✓ │ │ └─ 否 → 保持 Next-Key Lock │ └─ 否 → 保持 Next-Key Lock │ └─ 否 → 保持 Next-Key Lock ↓加锁完成4.2 示例:完整加锁过程
Section titled “4.2 示例:完整加锁过程”-- 表结构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. 各种场景分析
Section titled “5. 各种场景分析”5.1 场景 1: 主键等值查询(存在)
Section titled “5.1 场景 1: 主键等值查询(存在)”-- 数据:id = 5, 10, 15, 20SELECT * 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, 20SELECT * 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) -- 间隙被锁5.3 场景 3: 非唯一索引等值查询
Section titled “5.3 场景 3: 非唯一索引等值查询”-- 数据: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 -- 间隙被锁5.4 场景 4: 主键范围查询
Section titled “5.4 场景 4: 主键范围查询”-- 数据:id = 5, 10, 15, 20SELECT * 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 -- 间隙被锁5.5 场景 5: 唯一索引范围查询
Section titled “5.5 场景 5: 唯一索引范围查询”-- 数据:id = 5, 10, 15, 20SELECT * 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, +∞) 的所有间隙- 完全防止幻读5.6 对比总结表
Section titled “5.6 对比总结表”| 场景 | 查询类型 | 记录状态 | 加锁类型 | 示例 |
|---|---|---|---|---|
| 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 |