当前读与锁机制:记录锁、间隙锁、临键锁与意向锁(122)

MySQL与关系型数据库约 6 分钟阅读3,420 字符
知识库文档 123 / 251

阅读时间:约 45 分钟 | 难度:中高级 | 编号:122


一、引言:锁是并发控制的最后一道防线

MVCC 让读操作不加锁,但写操作(以及带锁的读)必须通过锁机制来保证数据一致性。理解 InnoDB 的锁机制,是排查死锁、锁等待、并发性能问题的前提。

InnoDB 的锁体系: 表级锁(意向锁)→ 行级锁(记录锁、间隙锁、临键锁)

文章配图
文章配图


二、当前读:锁的触发者

2.1 什么是当前读

当前读(Current Read) 读取的是数据的最新版本,并对读取的行加锁。

sql
SELECT * FROM t WHERE id = 1 FOR UPDATE;  -- 当前读,加 X 锁
SELECT * FROM t WHERE id = 1 LOCK IN SHARE MODE;  -- 当前读,加 S 锁
UPDATE t SET name = 'Bob' WHERE id = 1;  -- 当前读,加 X 锁
DELETE FROM t WHERE id = 1;  -- 当前读,加 X 锁
INSERT INTO t VALUES (1, 'Alice');  -- 不加锁,但可能被间隙锁阻塞

2.2 当前读与 MVCC 的关系

当前读不使用 Read View,而是读取最新提交的版本。如果最新版本由未提交的事务创建,当前读会等待该事务提交或回滚。

文章配图
文章配图


三、记录锁(Record Lock)

3.1 记录锁的定义

记录锁锁定索引中的一条记录。 它是最基本的行锁。

sql
-- 对 id=10 的记录加 X 锁
SELECT * FROM t WHERE id = 10 FOR UPDATE;

3.2 记录锁的特征

  • 锁对象是索引记录,不是数据行
  • 如果表没有索引,InnoDB 使用隐藏的聚簇索引加锁
  • 记录锁只阻止其他事务对该行的修改和加锁

3.3 记录锁的加锁条件

条件 锁类型
唯一索引等值查询,记录存在 记录锁
唯一索引等值查询,记录不存在 间隙锁
普通索引等值查询 临键锁

文章配图
文章配图


四、间隙锁(Gap Lock)

4.1 间隙锁的定义

间隙锁锁定索引记录之间的间隙,防止其他事务在间隙中插入新记录。

code
索引记录: 5, 10, 15, 20, 25
间隙: (-∞, 5), (5, 10), (10, 15), (15, 20), (20, 25), (25, +∞)

4.2 间隙锁的特点

  • 间隙锁之间不冲突:两个事务可以同时持有同一个间隙的间隙锁
  • 间隙锁只阻止 INSERT,不阻止 UPDATE 和 DELETE
  • 间隙锁在 READ COMMITTED 隔离级别下不存在

4.3 间隙锁的产生场景

sql
-- 唯一索引等值查询,记录不存在
SELECT * FROM t WHERE id = 7 FOR UPDATE;  -- 锁住 (5, 10) 间隙

-- 范围查询
SELECT * FROM t WHERE id > 15 FOR UPDATE;  -- 锁住 (15, 20), (20, +∞)

文章配图
文章配图


五、临键锁(Next-Key Lock)

5.1 临键锁的定义

临键锁 = 记录锁 + 间隙锁。 它是 InnoDB 默认的行锁算法,用于解决幻读。

code
索引记录: 5, 10, 15
SELECT * FROM t WHERE id = 10 FOR UPDATE(普通索引);
→ 临键锁: (5, 10] + (10, 15]
→ 锁住了 id=10 的记录,以及 (5, 10) 和 (10, 15) 的间隙

5.2 临键锁的退化规则

条件 锁类型
唯一索引,等值查询,记录存在 退化为记录锁
唯一索引,等值查询,记录不存在 退化为间隙锁
唯一索引,范围查询 临键锁,右边界退化为间隙锁
普通索引,等值查询 临键锁
普通索引,范围查询 临键锁

5.3 为什么唯一索引等值查询退化为记录锁?

唯一索引保证不会有重复值,不需要锁住间隙——因为其他事务不可能插入相同的唯一键值。所以只锁住记录就够了。

文章配图
文章配图


六、意向锁(Intention Lock)

6.1 意向锁的定义

意向锁是表级锁,用于协调行锁和表锁的冲突。

  • 意向共享锁(IS):事务打算在某些行上加 S 锁
  • 意向排他锁(IX):事务打算在某些行上加 X 锁

6.2 为什么需要意向锁?

假设没有意向锁,一个事务想对表加表级 X 锁(如 LOCK TABLES t WRITE),需要逐行检查是否有其他事务持有行锁。如果表有 1000 万行,这个检查代价太大。

有意向锁后: 只要检查表上是否有冲突的意向锁即可。如果表上没有 IX 锁,说明没有事务持有行级 X 锁,可以直接加表级 X 锁。

6.3 意向锁的兼容矩阵

IS IX S X
IS ✅ ✅ ✅ ❌
IX ✅ ✅ ❌ ❌
S ✅ ❌ ✅ ❌
X ❌ ❌ ❌ ❌

文章配图
文章配图


七、加锁规则实战

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

默认使用临键锁,然后根据条件退化。

7.2 原则二:查找过程中访问到的对象都要加锁

即使不满足 WHERE 条件的记录,只要在索引扫描过程中被访问到,也要加锁。

sql
-- 索引 idx_a (a)
SELECT * FROM t WHERE a = 5 FOR UPDATE;
-- 如果 idx_a 扫描了 a=1,3,5,7,9,即使只返回 a=5,也会对所有扫描到的行加锁

7.3 原则三:等值查询向右遍历到不满足条件为止

sql
-- 索引 idx_a (a): 1, 3, 5, 7, 9
SELECT * FROM t WHERE a = 5 FOR UPDATE;
-- 等值查询:从 a=5 开始向右遍历,直到 a=7(不满足条件)
-- 锁住 (3, 5], (5, 7)

7.4 特殊规则:唯一索引等值查询退化为记录锁

sql
-- 主键索引
SELECT * FROM t WHERE id = 5 FOR UPDATE;
-- 只锁 id=5 这一行(记录锁),不锁间隙

文章配图
文章配图


八、锁监控与排查

8.1 查看锁等待

sql
-- 当前锁等待
SELECT * FROM performance_schema.data_locks;

-- 锁等待关系
SELECT * FROM performance_schema.data_lock_waits;

-- 事务信息
SELECT * FROM information_schema.innodb_trx;

8.2 死锁日志

sql
-- 查看最近一次死锁
SHOW ENGINE INNODB STATUS\G
-- 搜索 "LATEST DETECTED DEADLOCK"

文章配图
文章配图


九、总结

锁类型 粒度 作用 冲突
记录锁 行 锁住一行 阻止修改
间隙锁 间隙 阻止插入 间隙锁之间不冲突
临键锁 行+间隙 记录锁+间隙锁 阻止修改和插入
意向锁 表 协调行锁和表锁 按兼容矩阵

核心原则: 默认临键锁,根据条件退化。唯一索引等值→记录锁,不存在→间隙锁,范围→临键锁。


十、面试官视角提问

Q1:**SELECT * FROM t WHERE id>5 FOR UPDATE**,索引记录为 1,3,5,7,9。锁住的范围是什么?如果另一个事务**INSERT INTO t VALUES (6, ...)**会被阻塞吗?

Q2:两个事务同时执行**SELECT * FROM t WHERE id=7 FOR UPDATE**(id=7 不存在,索引记录为 5,10)。两个事务会互相阻塞吗?为什么?

Q3:READ COMMITTED 隔离级别下,间隙锁还存在吗?为什么 RC 下不需要间隙锁?

Q4:**SELECT * FROM t WHERE a=5 FOR UPDATE**,a 是普通索引,值为 1,3,5,5,7,9。锁住的范围是什么?为什么?

Q5:一个事务执行**SELECT * FROM t WHERE id>5 FOR UPDATE**,另一个事务执行**UPDATE t SET name='X' WHERE id=3**。后者会被阻塞吗?为什么?

Q6:**LOCK TABLES t WRITE**和**SELECT ... FOR UPDATE**有什么区别?从锁的粒度和并发影响分析。


作者注:本文为系列第 122 篇,属于「08 MySQL 与关系型数据库」分类。


十一、面试官提问 · 参考答案

Q1:,索引 1,3,5,7,9。锁住范围?INSERT 6 会被阻塞吗?

参考答案:

锁住范围: 记录锁 id=7, 9。间隙锁 (5, 7), (7, 9), (9, +∞)。临键锁:(5, 7], (7, 9], (9, +∞]

INSERT id=6: 会被阻塞。id=6 落在 (5, 7) 间隙中,被间隙锁锁定。

INSERT id=3: 不会被阻塞。id=3 在 (1, 3) 中,不在锁范围内。

INSERT id=10: 会被阻塞。id=10 落在 (9, +∞) 间隙中。


Q2:两个事务同时 (id=7 不存在),会互相阻塞吗?

参考答案:

不会阻塞。 间隙锁之间不冲突。

两个事务都获取了 (5, 10) 的间隙锁,间隙锁之间兼容。这是 InnoDB 的设计——"阻止插入"不需要互斥,多个事务可以同时持有同一个间隙锁。

但如果一个事务尝试 INSERT id=7: 会被阻塞,因为间隙锁阻止 INSERT。


Q3:READ COMMITTED 下间隙锁还存在吗?

参考答案:

不存在。 RC 下只使用记录锁,不使用间隙锁。

原因: RC 允许不可重复读和幻读,不需要间隙锁来防止幻读。RC 只需要保证不读脏数据,记录锁就足够了。

影响: RC 的并发性能比 RR 高(没有间隙锁阻塞 INSERT),但可能出现幻读。


Q4:,a 是普通索引值为 1,3,5,5,7,9。锁住范围?

参考答案:

临键锁: (3, 第一个5], (第一个5, 第二个5], (第二个5, 7]

具体: 普通索引允许重复值,需要锁住所有 a=5 的记录以及它们之间的间隙,防止其他事务插入新的 a=5 的记录。

如果 a 是唯一索引: 退化为记录锁,只锁 a=5 那一条记录。


Q5:事务 A 执行 ,事务 B 执行 。B 会被阻塞吗?

参考答案:

不会被阻塞。 id=3 在 id>5 的锁范围之外。

锁范围是 (5, +∞),id=3 在 (1, 5) 中,不在锁范围内。UPDATE id=3 需要获取 id=3 的记录锁,这个锁没有被事务 A 持有。

但如果事务 B 执行**UPDATE t SET name='X' WHERE id=7**: 会被阻塞,id=7 在锁范围内。


Q6: 和 有什么区别?

参考答案:

维度 LOCK TABLES t WRITE SELECT ... FOR UPDATE
粒度 整个表 行(+ 间隙)
并发 完全排他,其他连接无法读写 只锁符合条件的行
事务 自动提交下生效 必须在事务中
意向锁 不需要,直接加表锁 需要 IX 意向锁
适用场景 全表备份、批量导入 行级并发控制

LOCK TABLES 是"核武器"—— 会阻塞所有其他连接对该表的任何操作。除非必要,否则使用行锁。


全文完 | 编号 122 | 分类 08 MySQL 与关系型数据库