临键锁 (Next-Key Lock)
📋 概述
临键锁(Next-Key Lock)是 MySQL InnoDB 引擎在 REPEATABLE READ 隔离级别下使用的默认锁算法。它是记录锁(Record Lock)和间隙锁(Gap Lock)的组合,既能锁定索引记录本身,又能锁定记录之前的间隙。
核心特点
✅ 优点:
- 彻底防止幻读: 同时锁定记录和间隙
- 保证可重复读: 确保事务内多次查询结果一致
- 默认启用: InnoDB 的默认行为,无需配置
- 左开右闭区间: 精确的锁定范围定义
❌ 缺点:
- 并发度低: 阻塞较大范围的插入
- 仅 MySQL: InnoDB 特有机制
- 性能影响: 高并发场景性能下降
- 理解复杂: 锁定范围较难准确判断
适用场景
- REPEATABLE READ 隔离级别: MySQL 默认隔离级别
- 范围查询: 需要防止幻读的场景
- 一致性要求高: 确保事务内数据一致性
- 默认场景: InnoDB 的所有当前读操作
🔧 工作原理
锁定范围:左开右闭区间
临键锁的锁定范围是左开右闭区间 (gap, record]:
索引记录: Record(1) --- Record(3) --- Record(5) --- Record(7)
临键锁范围:
- 第一个临键锁:(-∞, 1]
- 第二个临键锁:(1, 3]
- 第三个临键锁:(3, 5]
- 第四个临键锁:(5, 7]
- 第五个临键锁:(7, +∞)解读:
(1, 3]表示:锁定大于 1 且小于等于 3 的范围- 包括记录 3 本身(记录锁)
- 包括间隙 (1, 3)(间隙锁)
示例说明
sql
-- 假设表中有 id = 1, 3, 5, 7 的记录
-- 执行范围查询
SELECT * FROM users WHERE id BETWEEN 3 AND 5 FOR UPDATE;InnoDB 的锁定行为:
| 锁类型 | 锁定范围 | 说明 |
|---|---|---|
| 临键锁 1 | (1, 3] | 锁定记录 3 + 间隙 (1, 3) |
| 临键锁 2 | (3, 5] | 锁定记录 5 + 间隙 (3, 5) |
| 临键锁 3 | (5, 7] | 锁定间隙 (5, 7),防止插入 |
效果:
- ✅ 其他事务不能修改 id=3 或 id=5 的记录
- ✅ 其他事务不能插入 id 在 (1, 7] 范围内的新记录
- ❌ 其他事务可以插入 id < 1 或 id > 7 的记录
💻 MySQL InnoDB 实现
基本用法
sql
-- 设置隔离级别为 REPEATABLE READ(默认)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 范围查询自动使用临键锁
BEGIN;
SELECT * FROM users WHERE id BETWEEN 3 AND 7 FOR UPDATE;
-- 锁定:
-- - 记录 3, 5, 7(记录锁)
-- - 间隙 (1, 3), (3, 5), (5, 7), (7, +∞)(间隙锁)
COMMIT;
-- 等值查询也使用临键锁(非唯一索引)
SELECT * FROM users WHERE age = 18 FOR UPDATE;
-- 锁定所有 age=18 的记录及其前后的间隙查看临键锁
sql
-- 查看临键锁
SELECT
lock_id,
lock_trx_id,
lock_mode,
lock_type,
lock_table,
lock_index,
lock_data
FROM performance_schema.data_locks
WHERE lock_type = 'RECORD';输出示例:
lock_mode | lock_data
----------------|----------
X | 3
X,GAP | 5解读:
- 第一行:记录 3 上的排他锁(记录锁部分)
- 第二行:记录 5 之前的间隙上的排他锁(间隙锁部分)
- 合起来就是临键锁
📊 临键锁 vs 记录锁 vs 间隙锁
对比表
| 特性 | 记录锁 | 间隙锁 | 临键锁 |
|---|---|---|---|
| 锁定记录 | ✅ | ❌ | ✅ |
| 锁定间隙 | ❌ | ✅ | ✅ |
| 防止幻读 | ❌ | ✅ | ✅ |
| 并发度 | 高 | 中 | 低 |
| 隔离级别 | 所有 | RR | RR |
关系图
临键锁 = 记录锁 + 间隙锁
例如:锁定 (3, 5]
- 记录锁部分:锁定记录 5
- 间隙锁部分:锁定间隙 (3, 5)🎯 最佳实践
✅ 推荐做法
1. 使用唯一索引避免不必要的临键锁
sql
-- ✅ 创建唯一索引
CREATE UNIQUE INDEX idx_unique_id ON users(id);
-- 唯一索引的等值查询只加记录锁,不加临键锁
SELECT * FROM users WHERE id = 3 FOR UPDATE;
-- 只锁定记录 3,不锁定间隙2. 选择合适的隔离级别
sql
-- 如果不需要防止幻读,使用 READ COMMITTED
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- READ COMMITTED 级别下只使用记录锁,不使用临键锁3. 优化查询减少锁定范围
sql
-- ❌ 不推荐:大范围查询
SELECT * FROM users WHERE id BETWEEN 1 AND 1000 FOR UPDATE;
-- ✅ 推荐:缩小范围
SELECT * FROM users WHERE id BETWEEN 1 AND 10 FOR UPDATE;❌ 避免陷阱
1. 避免临键锁导致的插入阻塞
sql
-- 事务 A
BEGIN;
SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 锁定了临键锁 (10, 20] 和 (20, next_record]
-- 事务 B
INSERT INTO users VALUES (15, 'john');
-- ❌ 被阻塞!15 在临键锁范围内
-- 解决:等待事务 A 提交,或使用 READ COMMITTED2. 注意临键锁的死锁风险
sql
-- 事务 A
BEGIN;
SELECT * FROM users WHERE id = 5 FOR UPDATE; -- 临键锁 (3, 5]
INSERT INTO users VALUES (4, 'test'); -- 尝试插入到 (3, 5)
-- 事务 B
BEGIN;
SELECT * FROM users WHERE id = 3 FOR UPDATE; -- 临键锁 (1, 3]
INSERT INTO users VALUES (4, 'test'); -- 尝试插入到 (3, 5)
-- 结果:可能死锁🔍 监控与诊断
sql
-- 查看临键锁
SELECT
l.lock_id,
l.lock_mode,
l.lock_index,
l.lock_data,
t.trx_id,
t.trx_state
FROM performance_schema.data_locks l
JOIN information_schema.innodb_trx t ON l.lock_trx_id = t.trx_id
WHERE l.lock_type = 'RECORD'
ORDER BY t.trx_started;
-- 查看锁等待
SELECT * FROM performance_schema.data_lock_waits;🚨 常见问题
Q1: 临键锁和间隙锁的区别?
答:
- 间隙锁: 只锁定间隙,不锁定记录
- 临键锁: 既锁定记录,又锁定间隙
- 关系: 临键锁 = 记录锁 + 间隙锁
Q2: 如何禁用临键锁?
答:
- 使用 READ COMMITTED 隔离级别
sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;- 使用唯一索引的等值查询
sql
SELECT * FROM users WHERE id = 3 FOR UPDATE; -- id 是唯一索引Q3: 临键锁的性能影响大吗?
答:
影响较大,特别是:
- 高并发插入场景
- 大范围查询
- 长事务持有锁
优化建议:
- 使用 READ COMMITTED
- 优化查询减少范围
- 缩短事务时间
📚 相关资源
内部链接
外部资源
最后更新: 2026-04-12
维护状态: ✅ 完整