第 6 讲:InnoDB 锁机制 -- 行锁、表锁、间隙锁、临键锁、死锁
核心结论(8 条必记)
- 普通 SELECT(快照读)靠 MVCC,不怎么加锁 -- 不涉及锁冲突
- 当前读(for update / update / delete / insert)会加锁 -- 用锁保证并发一致性
- 意向锁是表级"标记",自动加,用于快速判断表锁冲突 -- 写 SQL 时不用管
- 间隙锁用于阻止范围内插入新行 -- 防幻读
- 临键锁 = 记录锁 + 前间隙锁 -- RR 下范围当前读的典型加锁方式
- 等值 + 唯一索引只加记录锁 -- 不锁间隙
- 没有合适索引会导致锁范围变大 -- 甚至出现"像锁表一样"的效果
- 锁的粒度取决于查询条件能否高效利用索引 -- 这是理解加锁行为的核心
一、InnoDB 锁体系全局理解
表级意向锁(Intention Locks)
为什么需要意向锁?
事务 A 正在对某一行加行锁:
SELECT * FROM t WHERE id = 20 FOR UPDATE;此时事务 A 在 id=20 这一行上加了行级排他锁(X 锁)。
事务 B 想对整张表加表锁:
LOCK TABLES t WRITE;如果没有意向锁,InnoDB 要怎么知道"这张表上有没有行锁"?
它只能:
- 扫描整张表的所有行
- 看看有没有行锁存在
这效率太低了。
意向锁的作用
InnoDB 引入了表级意向锁。当事务 A 想对某些行加 X 锁时:
- 先在表级别自动加一个 IX(意向排他锁)
- 再在行级别加真正的 X 锁
事务 B 想加表锁时:
- 只需要看表级别有没有意向锁
- 如果有 IX,说明表里有行被锁了
- 就知道不能直接加表锁
不需要扫描所有行。
两种意向锁
IS(Intention Shared Lock)
表示:我准备对表里的某些行加共享锁(S 锁)
SELECT * FROM t WHERE id = 20 LOCK IN SHARE MODE;会先在表上加 IS,再在行上加 S。
IX(Intention Exclusive Lock)
表示:我准备对表里的某些行加排他锁(X 锁)
SELECT * FROM t WHERE id = 20 FOR UPDATE;
UPDATE ...
DELETE ...会先在表上加 IX,再在行上加 X。
意向锁之间的兼容性
意向锁之间互相兼容:
| IS | IX | |
|---|---|---|
| IS | 兼容 | 兼容 |
| IX | 兼容 | 兼容 |
也就是说:
- 多个事务可以同时持有 IS
- 多个事务可以同时持有 IX
- IS 和 IX 之间也兼容
意向锁和表锁的冲突
意向锁真正起作用是在和表级 S 锁 / X 锁做判断时:
| 表级 S | 表级 X | |
|---|---|---|
| IS | 兼容 | 冲突 |
| IX | 冲突 | 冲突 |
一句话总结
意向锁是 InnoDB 在表级别自动加的"标记",用来告诉别人"我正在或即将对某些行加锁",便于快速判断表锁冲突,你写 SQL 时不用管它,它会自动发生。
你平时几乎感知不到意向锁,因为:
- 你不会显式写
LOCK TABLES(线上很少用) - InnoDB 默认是行锁
- 意向锁只是内部协调机制
行级锁(决定并发的核心)
| 锁类型 | 锁住的对象 | 用途 |
|---|---|---|
| 记录锁(Record Lock) | 某条索引记录 | 锁住具体行 |
| 间隙锁(Gap Lock) | 索引记录之间的空隙 | 防止范围内插入 |
| 临键锁(Next-Key Lock) | 记录锁 + 前面的间隙锁 | RR 下防幻读 |
InnoDB 的行锁围绕索引工作,没有合适索引就会锁得更大
二、哪些语句会加锁
触发锁的语句(当前读)
SELECT ... FOR UPDATE;
SELECT ... LOCK IN SHARE MODE;
UPDATE ...
DELETE ...
INSERT ...不触发锁的语句(快照读)
SELECT * FROM user WHERE id = 1; -- 普通 select,走 MVCC| 类型 | SQL | 是否加锁 | 机制 |
|---|---|---|---|
| 快照读 | select ... | 不加锁 | MVCC |
| 当前读 | select ... for update 等 | 加锁 | 锁机制 |
三、间隙锁(Gap Lock)
为什么需要"锁住空隙"?
RR 隔离级别 + 范围当前读场景:
索引记录: (id=10), (id=20), (id=30)SELECT * FROM t WHERE id > 10 AND id < 25 FOR UPDATE;为了防幻读,InnoDB 会锁住 (20, 30) 这样的间隙区间。
此时另一个事务插入 id=22:
INSERT INTO t(id, ...) VALUES(22, ...);
-- 被阻塞!落入间隙锁保护的区间一句话总结
间隙锁 = 禁止其他事务把新记录插进你当前锁定的范围里
四、临键锁(Next-Key Lock)
锁住什么?
临键锁 = 锁住某条记录 + 锁住该记录前面的间隙
索引记录: 10, 20, 30
临键锁锁住 id=20 时:
间隙 (10, 20) + 记录 20作用
在 RR 下对范围做当前读时,严密防止"幻行"。即使是"插入刚好落到边界附近",也会被拦住。
面试常用答法:Next-Key Lock 用来防止在 RR 的当前读下发生幻读
五、等值查询 vs 范围查询的加锁差异
等值查询 + 唯一索引 -> 只加记录锁
-- id 是主键/唯一索引
UPDATE t SET ... WHERE id = 20;
-- 一般只锁 id=20 这一条记录范围查询 -> 触发 gap / next-key
UPDATE t SET ... WHERE id > 10 AND id < 25;
-- 会锁范围内的记录 + 间隙| 查询类型 | 索引情况 | 加锁范围 |
|---|---|---|
| 等值 + 唯一索引 | 精确匹配 | 记录锁(最小) |
| 等值 + 普通索引 | 可能多行匹配 | 记录锁 + gap |
| 范围查询 | 走索引 | 记录锁 + gap + next-key |
| 无索引 | 全表扫描 | 锁大量行/范围(接近锁表) |
六、InnoDB "表锁"是怎么来的
InnoDB 默认是行锁,但以下情况锁范围会很大:
- 没有合适索引 -> 执行计划扫描大量行,锁住更多记录/范围
- 查询条件无法走索引 -> 只能锁住更大范围
- 显式 LOCK TABLES -> 真正的表锁(一般不用)
- MDL(元数据锁) -> DDL 改表结构时发生,与业务行锁不同类
排查"锁等待"时,要看是在等行锁(record/gap/next-key)还是MDL
七、死锁
7.1 什么是死锁
两个事务彼此持有对方需要的锁,谁也等不到,只能回滚其中一个。
7.2 经典死锁场景
表有行 id=1 和 id=2:
T1: UPDATE ... WHERE id=1; T2: UPDATE ... WHERE id=2;
(持有 id=1 的锁) (持有 id=2 的锁)
T1: UPDATE ... WHERE id=2; T2: UPDATE ... WHERE id=1;
(等 T2 释放 id=2) (等 T1 释放 id=1)
-> 循环等待 = 死锁7.3 InnoDB 怎么处理
- 有死锁检测机制
- 发现后选择代价较小的事务回滚,让另一个继续
- 工程上常见策略:捕获死锁异常,做幂等重试
7.4 怎么排查
SHOW ENGINE INNODB STATUS; -- 看死锁日志- 结合 performance_schema 查 lock wait
- 结合事务 SQL、锁住的索引与范围分析
7.5 怎么避免
| 策略 | 说明 |
|---|---|
| 固定资源获取顺序 | 永远先更新 id 小的再更新大的 |
| 缩短事务时间 | 减少持锁时间 |
| 走合适索引 | 避免锁大量行/范围 |
| 减少单次锁的行数 | 批量操作不要全量扫 |
| 失败重试 | 死锁是常态化处理 |
八、完整加锁流程示意
事务 A 执行:
SELECT * FROM t WHERE id > 10 AND id < 25 FOR UPDATE;1. 表级
自动加 IX(意向排他锁)
2. 行级
对范围内的记录和间隙加:
- 记录锁(锁住已存在的记录)
- 间隙锁(锁住范围内的空隙)
- 临键锁(记录 + 前间隙)
意向锁 + 行级锁 = 完整的加锁过程
九、面试高频题
1. InnoDB 有哪些锁?
行级锁:记录锁、间隙锁、临键锁。表级:意向锁、MDL。核心是行锁围绕索引工作。
2. 间隙锁是什么?为什么需要?
锁住索引记录之间的间隙,阻止其他事务在范围内插入新行。为了在 RR 下防止幻读。
3. 临键锁是什么?
记录锁 + 前间隙锁的组合。RR 下对范围做当前读时,严密防止幻行。
4. 为什么 UPDATE 有时只锁一行,有时锁很多?
取决于执行计划和索引。等值 + 唯一索引只加记录锁;无索引或范围查询会加更大范围的锁。
5. 死锁怎么处理?
InnoDB 自动检测并回滚代价小的事务。工程上要保证资源获取顺序一致、事务短小、可重试。
十、面试标准答案模板
InnoDB 主要有表级意向锁和行级锁。 意向锁(IS/IX)用于快速判断表锁冲突,自动加在表上。 行级锁包括记录锁、间隙锁和临键锁。 记录锁锁住单条记录,间隙锁锁住索引间隙防止插入,临键锁是两者组合。 在 RR 隔离级别下,范围当前读会用间隙锁/临键锁防止幻读。 锁的粒度取决于查询条件能否高效利用索引,没有索引会导致锁范围变大。
十一、查询条件 -> 锁类型决策表
| 查询条件 | 索引情况 | 锁类型 |
|---|---|---|
| 等值 + 主键/唯一索引 | 能精确定位 | 记录锁(单行) |
| 等值 + 普通索引 | 可能多行匹配 | 记录锁 + 可能间隙锁 |
| 范围查询 | 有索引 | 记录锁 + 间隙锁 / 临键锁 |
| 没有索引 / 索引失效 | 全表扫描 | 锁大量行(接近锁表效果) |
核心判断规则:能精确定位 -> 锁一行;范围查询 -> 锁范围+间隙;无索引 -> 锁大量行
十二、练习题解析
练习 1:等值 + 主键
题目: 事务 A 执行 SELECT * FROM t WHERE id=20 FOR UPDATE(id 主键),另一事务插入 id=22 会怎样?
答案:不会阻塞
因为 id 是主键(唯一索引),并且是等值查询,事务 A 只会对 id=20 这一条记录加记录锁(Record Lock)。id=22 和 id=20 是不同的记录,不会被阻塞。
等值查询 + 唯一索引 → 通常只锁那一条记录
练习 2:范围查询
题目: 事务 A 执行 SELECT * FROM t WHERE id>10 AND id<25 FOR UPDATE,另一事务插入 id=22 会怎样?
答案:会阻塞
这是范围查询 + 当前读,隔离级别 RR 下,InnoDB 会用间隙锁 / 临键锁锁住这个范围。
假设索引上有 10, 20, 30:
- 锁住
(10, 20](临键锁) - 锁住
(20, 30)(间隙锁)
id=22 落在这个范围内,会被间隙锁阻塞。
范围查询 → 锁住范围内的记录 + 锁住这个范围内的间隙,防止别人插入新行
练习 3:锁粒度差异
题目: 为什么"同样是 UPDATE",有时只锁一行,有时锁范围很多?
答案:锁的粒度取决于查询条件能不能高效利用索引
| 查询条件 | 索引情况 | 锁类型 |
|---|---|---|
| 等值 + 主键/唯一索引 | 能精确定位 | 记录锁(单行) |
| 等值 + 普通索引 | 可能多行 | 记录锁 + 可能间隙锁 |
| 范围查询 | 有索引 | 记录锁 + 间隙锁 / 临键锁 |
| 没有索引 / 索引失效 | 全表扫描 | 锁大量行(接近锁表效果) |
核心判断规则:
- 能精确定位(等值 + 唯一索引)→ 锁一行
- 范围查询 → 锁范围 + 间隙
- 非唯一索引 → 锁多行 + 可能锁间隙
- 没索引 / 索引失效 → 锁很多行,甚至扫全表
下一讲预告
第 7 讲:慢查询优化与性能调优
- 怎么定位慢 SQL
- 深分页为什么慢、怎么优化
- count(*) 为什么有时慢
- join 优化
- 批量写入优化
- 大事务问题