Skip to content

间隙锁 (Gap Lock) ​

📋 概述 ​

间隙锁(Gap Lock)是 MySQL InnoDB 引擎特有的一种锁算法,它锁定的不是索引记录本身,而是索引记录之间的间隙。间隙锁的主要作用是防止其他事务在间隙中插入新记录,从而避免"幻读"现象。

核心特点 ​

✅ 优点:

  • 防止幻读: 阻止其他事务在间隙中插入新记录
  • 保证一致性: 确保范围查询的可重复性
  • 实现简单: 只需锁定间隙,无需额外数据结构
  • 按需启用: 只在 REPEATABLE READ 隔离级别下生效

❌ 缺点:

  • 降低并发: 阻塞 INSERT 操作
  • 仅 MySQL: 其他数据库使用不同机制
  • 可能过度锁定: 即使没有记录也会锁定间隙
  • 性能影响: 高并发插入场景性能下降

适用场景 ​

  • REPEATABLE READ 隔离级别: MySQL 默认隔离级别
  • 范围查询: WHERE id BETWEEN 1 AND 10
  • 防止幻读: 需要确保多次查询结果一致
  • 唯一索引检查: 检查唯一约束时

🔧 工作原理 ​

间隙的概念 ​

索引结构:
Record(-∞) --- Record(1) --- Record(3) --- Record(5) --- Record(+∞)
     |            |              |              |             |
   间隙1        间隙2          间隙3          间隙4         间隙5

间隙定义:

  • 间隙1: (-∞, 1) - 小于 1 的范围
  • 间隙2: (1, 3) - 1 和 3 之间的范围
  • 间隙3: (3, 5) - 3 和 5 之间的范围
  • 间隙4: (5, +∞) - 大于 5 的范围

间隙锁的锁定范围 ​

sql
-- 假设表中有 id = 1, 3, 5, 7 的记录

-- 查询 id = 3 的记录并加锁
SELECT * FROM users WHERE id = 3 FOR UPDATE;

InnoDB 的锁定行为(REPEATABLE READ 级别):

  • 锁定 id=3 的记录(记录锁)
  • 锁定间隙 (1, 3)(间隙锁)
  • 锁定间隙 (3, 5)(间隙锁)

效果:

  • 其他事务不能插入 id 在 (1, 5) 范围内的新记录
  • 其他事务可以插入 id < 1 或 id > 5 的记录

纯间隙锁 (Pure Gap Lock) ​

某些情况下,间隙锁只锁定间隙,不锁定记录:

sql
-- 查询不存在的记录
SELECT * FROM users WHERE id = 4 FOR UPDATE;
-- id=4 不存在,但仍会锁定间隙 (3, 5)

作用: 防止其他事务插入 id=4 的记录


💻 MySQL InnoDB 实现 ​

基本用法 ​

sql
-- 设置隔离级别为 REPEATABLE READ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 范围查询加锁
BEGIN;
SELECT * FROM users WHERE id BETWEEN 1 AND 10 FOR UPDATE;
-- 锁定:
-- 1. id 在 [1, 10] 范围内的所有记录(记录锁)
-- 2. 间隙 (-∞, 1) 的前部分
-- 3. 间隙 (10, +∞) 的前部分
COMMIT;

-- 等值查询(记录不存在)
SELECT * FROM users WHERE id = 4 FOR UPDATE;
-- 锁定间隙 (3, 5),假设存在 id=3 和 id=5

查看间隙锁 ​

sql
-- 查看间隙锁(通过 performance_schema)
SELECT 
    lock_id,
    lock_trx_id,
    lock_mode,
    lock_type,
    lock_table,
    lock_index,
    lock_data
FROM performance_schema.data_locks
WHERE lock_mode LIKE '%GAP%';

输出示例:

lock_mode       | lock_data
----------------|----------
X,GAP           | 3

解读:

  • lock_mode = X,GAP: 排他间隙锁
  • lock_data = 3: 锁定的是 id=3 之后的间隙

📊 间隙锁的影响范围 ​

不同查询类型的间隙锁 ​

1. 等值查询(记录存在) ​

sql
-- 假设存在 id = 1, 3, 5, 7
SELECT * FROM users WHERE id = 3 FOR UPDATE;

锁定范围:

  • 记录锁: id=3
  • 间隙锁: (1, 3) 和 (3, 5)

2. 等值查询(记录不存在) ​

sql
SELECT * FROM users WHERE id = 4 FOR UPDATE;

锁定范围:

  • 纯间隙锁: (3, 5)

3. 范围查询 ​

sql
SELECT * FROM users WHERE id BETWEEN 3 AND 7 FOR UPDATE;

锁定范围:

  • 记录锁: id=3, 5, 7
  • 间隙锁: (1, 3), (3, 5), (5, 7), (7, +∞) 的前部分

4. 唯一索引的等值查询 ​

sql
-- 假设 id 是唯一索引
SELECT * FROM users WHERE id = 3 FOR UPDATE;

锁定范围:

  • 只锁定记录 id=3(记录锁)
  • 不锁定间隙(因为唯一索引不需要防止插入)

注意: 唯一索引的等值查询不会加间隙锁!


🎯 最佳实践 ​

✅ 推荐做法 ​

1. 理解间隙锁的行为 ​

sql
-- ✅ 推荐:使用唯一索引避免不必要的间隙锁
CREATE UNIQUE INDEX idx_unique_id ON users(id);

-- 唯一索引的等值查询不会加间隙锁
SELECT * FROM users WHERE id = 3 FOR UPDATE;

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 1 AND 10 FOR UPDATE;
-- 锁定了间隙 (10, +∞) 的前部分

-- 事务 B
INSERT INTO users VALUES (15, 'john');
-- ❌ 被阻塞!虽然 15 不在 [1, 10] 范围内,但在间隙锁范围内

-- 解决:等待事务 A 提交,或使用 READ COMMITTED 隔离级别

2. 避免死锁 ​

sql
-- 事务 A
BEGIN;
SELECT * FROM users WHERE id = 5 FOR UPDATE;  -- 锁定间隙 (3, 5) 和 (5, 7)
INSERT INTO users VALUES (4, 'test');  -- 尝试在间隙 (3, 5) 插入

-- 事务 B
BEGIN;
SELECT * FROM users WHERE id = 3 FOR UPDATE;  -- 锁定间隙 (1, 3) 和 (3, 5)
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,
    t.trx_started
FROM performance_schema.data_locks l
JOIN information_schema.innodb_trx t ON l.lock_trx_id = t.trx_id
WHERE l.lock_mode LIKE '%GAP%'
ORDER BY t.trx_started;

-- 查看间隙锁导致的等待
SELECT * FROM performance_schema.data_lock_waits;

🚨 常见问题 ​

Q1: 间隙锁只在 MySQL 中存在吗? ​

答: 是的,间隙锁是 InnoDB 特有的。其他数据库使用不同机制:

  • PostgreSQL: 使用谓词锁(Predicate Lock)
  • SQL Server: 使用键范围锁(Key-Range Lock)
  • Oracle: 依赖 MVCC,不需要间隙锁

Q2: 如何避免间隙锁? ​

答:

  1. 使用 READ COMMITTED 隔离级别
sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
  1. 使用唯一索引的等值查询
sql
-- 唯一索引的等值查询不加间隙锁
SELECT * FROM users WHERE id = 3 FOR UPDATE;
  1. 使用快照读
sql
-- 普通 SELECT 不加锁
SELECT * FROM users WHERE id = 3;

Q3: 间隙锁会影响性能吗? ​

答: 会,特别是在高并发插入场景。

影响:

  • 阻塞 INSERT 操作
  • 增加锁等待时间
  • 可能导致死锁

优化建议:

  • 使用 READ COMMITTED 隔离级别
  • 优化查询减少锁范围
  • 批量插入时注意顺序

📚 相关资源 ​

内部链接 ​

外部资源 ​


最后更新: 2026-04-12
维护状态: ✅ 完整

Released under MIT License.