一、核心关系与概述
Next-Key Lock = 记录锁 + 间隙锁
这三者共同构成了 InnoDB 在 可重复读(Repeatable Read) 隔离级别下的锁定机制,用于解决并发事务中的各种数据一致性问题。
二、记录锁(Record Lock)
定义
记录锁是锁住索引记录本身的锁。当一条记录被锁定后,其他事务无法修改或删除这条记录。
特性
-
锁定对象:索引记录(不是数据行本身,而是索引条目)
-
作用:防止其他事务修改或删除已锁定的记录
-
兼容性:
-
共享记录锁(S)与共享记录锁兼容
-
排他记录锁(X)与任何记录锁都不兼容
-
-
加锁场景:
-
SELECT ... FOR UPDATE -
SELECT ... LOCK IN SHARE MODE -
UPDATE、DELETE语句
-
示例
sql
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 对 id=10 的记录加排他记录锁
-- 事务 B(被阻塞)
UPDATE users SET name = 'Bob' WHERE id = 10;
-- 需要等待事务 A 释放锁
三、间隙锁(Gap Lock)
定义
间隙锁锁定的是索引记录之间的间隙,防止在这个范围内插入新记录。
特性
-
锁定对象:索引记录之间的间隙
-
作用:防止幻读(Phantom Read)
-
锁定区间 :左开右开区间
(a, b) -
兼容性:
-
间隙锁之间不冲突(多个事务可以同时持有相同间隙的间隙锁)
-
间隙锁与插入意向锁冲突
-
-
加锁场景:
-
范围查询
-
等值查询未命中时
-
示例
sql
-- 表 users: id 索引,记录有 5, 10, 15
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id > 10 AND id < 15 FOR UPDATE;
-- 锁定间隙 (10, 15)
-- 事务 B(被阻塞)
INSERT INTO users (id, name) VALUES (12, 'Alice');
-- 尝试插入 id=12,位于间隙 (10, 15) 内,被阻塞
-- 事务 C(不阻塞)
INSERT INTO users (id, name) VALUES (20, 'Bob');
-- id=20 不在锁定范围内,可以正常插入
四、Next-Key Lock
定义
Next-Key Lock 是 InnoDB 的默认行锁算法,它结合了记录锁和间隙锁,锁定记录本身以及记录之前的间隙。
特性
-
锁定对象:记录 + 前一个间隙
-
锁定区间 :左开右闭区间
(a, b] -
作用 :同时防止不可重复读 和幻读
-
默认行为:InnoDB 在可重复读隔离级别下的默认锁定方式
-
扫描过程:
-
从第一个满足条件的记录开始
-
对扫描到的记录加 Next-Key Lock
-
继续扫描直到第一个不满足条件的记录,并对其加间隙锁
-
示例
sql
-- 表 users: id 主键,记录有 5, 10, 15, 20
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id >= 10 AND id < 15 FOR UPDATE;
-- 锁定的范围:
-- 1. 找到 id=10:锁定 Next-Key Lock (5, 10]
-- 2. 找到 id=15(不满足条件):锁定间隙锁 (10, 15)
-- 实际效果:锁定范围 (5, 15)
-- 事务 B(不同操作的阻塞情况)
-- 1. 被阻塞:插入 id=8(在 (5, 10] 范围内)
-- 2. 被阻塞:插入 id=12(在 (10, 15) 范围内)
-- 3. 通过:插入 id=3(不在锁定范围内)
-- 4. 被阻塞:更新 id=10(记录锁冲突)
五、不同查询场景下的锁定行为
1. 唯一索引等值查询
sql
-- id 是唯一索引,记录 id=10 存在
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 锁定:仅对 id=10 加记录锁(Next-Key Lock 退化为记录锁)
2. 唯一索引等值查询未命中
sql
-- id 是唯一索引,记录 id=12 不存在
SELECT * FROM users WHERE id = 12 FOR UPDATE;
-- 假设存在记录 10 和 15
-- 锁定:间隙锁 (10, 15)
3. 非唯一索引等值查询
sql
-- age 是非唯一索引,age=20 的记录存在
SELECT * FROM users WHERE age = 20 FOR UPDATE;
-- 锁定:
-- 1. 对 age=20 的所有记录加 Next-Key Lock
-- 2. 对 age=20 后的第一个不满足条件的记录加间隙锁
4. 范围查询
sql
-- id 是主键
SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 锁定:
-- 1. id=10:Next-Key Lock (前一个id, 10]
-- 2. 扫描到 id=20:Next-Key Lock (15, 20]
-- 3. 继续扫描到 id=25(不满足条件):间隙锁 (20, 25)
六、特殊情况与优化
1. 锁降级(Lock Degradation)
唯一索引等值查询命中:
sql
-- id 是唯一索引,且 id=10 存在
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- Next-Key Lock 退化为记录锁(只锁 id=10)
-- 因为唯一性保证不会有幻读
2. 锁升级
无索引查询:
sql
-- name 列无索引
SELECT * FROM users WHERE name = 'Alice' FOR UPDATE;
-- 无法使用行锁,会锁定全表的所有记录和间隙
-- 性能极差,应避免
3. 插入意向锁(Insert Intention Lock)
sql
-- 事务 A
SELECT * FROM users WHERE id > 10 FOR UPDATE;
-- 锁定间隙 (10, +∞)
-- 事务 B
INSERT INTO users (id, name) VALUES (15, 'Bob');
-- 需要获取插入意向锁,与事务 A 的间隙锁冲突
-- 事务 B 被阻塞
七、死锁示例分析
sql
-- 表结构:id 主键,记录有 5, 10
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id = 10 FOR UPDATE; -- 锁定 id=10
-- 事务 B
START TRANSACTION;
SELECT * FROM users WHERE id = 5 FOR UPDATE; -- 锁定 id=5
-- 事务 A
INSERT INTO users (id, name) VALUES (7, 'Alice');
-- 需要获取间隙锁 (5, 10),但事务 B 持有 id=5,可能涉及间隙锁
-- 等待事务 B
-- 事务 B
INSERT INTO users (id, name) VALUES (7, 'Bob');
-- 需要获取间隙锁 (5, 10),但事务 A 持有 id=10
-- 等待事务 A
-- 死锁发生!
八、实践建议
1. 索引设计
-
查询条件尽量使用索引
-
避免全表扫描导致锁表
-
合理设计唯一索引和非唯一索引
2. 事务设计
-
尽量缩短事务时间
-
避免在事务中执行长时间的操作
-
按固定顺序访问资源,避免死锁
3. SQL 优化
-
精确的查询条件减少锁定范围
-
避免范围查询锁定过多记录
-
使用
FOR UPDATE和LOCK IN SHARE MODE要谨慎
4. 监控与排查
sql
-- 查看当前锁信息
SHOW ENGINE INNODB STATUS;
-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
九、总结对比表
| 特性 | 记录锁 | 间隙锁 | Next-Key Lock |
|---|---|---|---|
| 锁定对象 | 索引记录 | 索引间隙 | 记录 + 前一个间隙 |
| 锁定区间 | 单点 | (a, b) 开区间 | (a, b] 左开右闭 |
| 主要目的 | 防止修改/删除 | 防止插入(幻读) | 防止修改/删除和插入 |
| 兼容性 | 记录锁间按规则冲突 | 间隙锁间不冲突 | 按包含的记录锁和间隙锁判断 |
| 使用场景 | 等值查询命中 | 范围查询、等值查询未命中 | InnoDB 默认行锁算法 |
| 隔离级别 | 所有支持行锁的级别 | RR 及以上(RC 通常禁用) | RR 级别默认使用 |
十、关键要点
-
Next-Key Lock 是 InnoDB 防止幻读的核心机制
-
间隙锁只在 RR 隔离级别有效(RC 级别通常不使用间隙锁)
-
唯一索引可以优化锁定范围(等值查询时退化为记录锁)
-
无索引查询会导致锁表,性能极差
-
插入意向锁与间隙锁冲突,是死锁的常见原因
理解这三种锁机制对于设计高性能、高并发的数据库应用至关重要,特别是在需要处理复杂事务的场景下。