阅读时间:约 45 分钟 | 难度:中高级 | 编号:122
一、引言:锁是并发控制的最后一道防线
MVCC 让读操作不加锁,但写操作(以及带锁的读)必须通过锁机制来保证数据一致性。理解 InnoDB 的锁机制,是排查死锁、锁等待、并发性能问题的前提。
InnoDB 的锁体系: 表级锁(意向锁)→ 行级锁(记录锁、间隙锁、临键锁)

二、当前读:锁的触发者
2.1 什么是当前读
当前读(Current Read) 读取的是数据的最新版本,并对读取的行加锁。
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 记录锁的定义
记录锁锁定索引中的一条记录。 它是最基本的行锁。
-- 对 id=10 的记录加 X 锁
SELECT * FROM t WHERE id = 10 FOR UPDATE;
3.2 记录锁的特征
- 锁对象是索引记录,不是数据行
- 如果表没有索引,InnoDB 使用隐藏的聚簇索引加锁
- 记录锁只阻止其他事务对该行的修改和加锁
3.3 记录锁的加锁条件
| 条件 | 锁类型 |
|---|---|
| 唯一索引等值查询,记录存在 | 记录锁 |
| 唯一索引等值查询,记录不存在 | 间隙锁 |
| 普通索引等值查询 | 临键锁 |

四、间隙锁(Gap Lock)
4.1 间隙锁的定义
间隙锁锁定索引记录之间的间隙,防止其他事务在间隙中插入新记录。
索引记录: 5, 10, 15, 20, 25
间隙: (-∞, 5), (5, 10), (10, 15), (15, 20), (20, 25), (25, +∞)
4.2 间隙锁的特点
- 间隙锁之间不冲突:两个事务可以同时持有同一个间隙的间隙锁
- 间隙锁只阻止 INSERT,不阻止 UPDATE 和 DELETE
- 间隙锁在 READ COMMITTED 隔离级别下不存在
4.3 间隙锁的产生场景
-- 唯一索引等值查询,记录不存在
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 默认的行锁算法,用于解决幻读。
索引记录: 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 条件的记录,只要在索引扫描过程中被访问到,也要加锁。
-- 索引 idx_a (a)
SELECT * FROM t WHERE a = 5 FOR UPDATE;
-- 如果 idx_a 扫描了 a=1,3,5,7,9,即使只返回 a=5,也会对所有扫描到的行加锁
7.3 原则三:等值查询向右遍历到不满足条件为止
-- 索引 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 特殊规则:唯一索引等值查询退化为记录锁
-- 主键索引
SELECT * FROM t WHERE id = 5 FOR UPDATE;
-- 只锁 id=5 这一行(记录锁),不锁间隙

八、锁监控与排查
8.1 查看锁等待
-- 当前锁等待
SELECT * FROM performance_schema.data_locks;
-- 锁等待关系
SELECT * FROM performance_schema.data_lock_waits;
-- 事务信息
SELECT * FROM information_schema.innodb_trx;
8.2 死锁日志
-- 查看最近一次死锁
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 与关系型数据库