Skip to content

第 6 讲:InnoDB 锁机制 -- 行锁、表锁、间隙锁、临键锁、死锁

核心结论(8 条必记)

  1. 普通 SELECT(快照读)靠 MVCC,不怎么加锁 -- 不涉及锁冲突
  2. 当前读(for update / update / delete / insert)会加锁 -- 用锁保证并发一致性
  3. 意向锁是表级"标记",自动加,用于快速判断表锁冲突 -- 写 SQL 时不用管
  4. 间隙锁用于阻止范围内插入新行 -- 防幻读
  5. 临键锁 = 记录锁 + 前间隙锁 -- RR 下范围当前读的典型加锁方式
  6. 等值 + 唯一索引只加记录锁 -- 不锁间隙
  7. 没有合适索引会导致锁范围变大 -- 甚至出现"像锁表一样"的效果
  8. 锁的粒度取决于查询条件能否高效利用索引 -- 这是理解加锁行为的核心

一、InnoDB 锁体系全局理解

表级意向锁(Intention Locks)

为什么需要意向锁?

事务 A 正在对某一行加行锁:

sql
SELECT * FROM t WHERE id = 20 FOR UPDATE;

此时事务 A 在 id=20 这一行上加了行级排他锁(X 锁)

事务 B 想对整张表加表锁:

sql
LOCK TABLES t WRITE;

如果没有意向锁,InnoDB 要怎么知道"这张表上有没有行锁"?

它只能:

  • 扫描整张表的所有行
  • 看看有没有行锁存在

这效率太低了。


意向锁的作用

InnoDB 引入了表级意向锁。当事务 A 想对某些行加 X 锁时:

  1. 先在表级别自动加一个 IX(意向排他锁)
  2. 再在行级别加真正的 X 锁

事务 B 想加表锁时:

  • 只需要看表级别有没有意向锁
  • 如果有 IX,说明表里有行被锁了
  • 就知道不能直接加表锁

不需要扫描所有行。


两种意向锁

IS(Intention Shared Lock)

表示:我准备对表里的某些行加共享锁(S 锁)

sql
SELECT * FROM t WHERE id = 20 LOCK IN SHARE MODE;

会先在表上加 IS,再在行上加 S。

IX(Intention Exclusive Lock)

表示:我准备对表里的某些行加排他锁(X 锁)

sql
SELECT * FROM t WHERE id = 20 FOR UPDATE;
UPDATE ...
DELETE ...

会先在表上加 IX,再在行上加 X。


意向锁之间的兼容性

意向锁之间互相兼容:

ISIX
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 的行锁围绕索引工作,没有合适索引就会锁得更大


二、哪些语句会加锁

触发锁的语句(当前读)

sql
SELECT ... FOR UPDATE;
SELECT ... LOCK IN SHARE MODE;
UPDATE ...
DELETE ...
INSERT ...

不触发锁的语句(快照读)

sql
SELECT * FROM user WHERE id = 1;  -- 普通 select,走 MVCC
类型SQL是否加锁机制
快照读select ...不加锁MVCC
当前读select ... for update加锁锁机制

三、间隙锁(Gap Lock)

为什么需要"锁住空隙"?

RR 隔离级别 + 范围当前读场景:

索引记录: (id=10), (id=20), (id=30)
sql
SELECT * FROM t WHERE id > 10 AND id < 25 FOR UPDATE;

为了防幻读,InnoDB 会锁住 (20, 30) 这样的间隙区间。

此时另一个事务插入 id=22

sql
INSERT INTO t(id, ...) VALUES(22, ...);
-- 被阻塞!落入间隙锁保护的区间

一句话总结

间隙锁 = 禁止其他事务把新记录插进你当前锁定的范围里


四、临键锁(Next-Key Lock)

锁住什么?

临键锁 = 锁住某条记录 + 锁住该记录前面的间隙

索引记录: 10, 20, 30

临键锁锁住 id=20 时:
  间隙 (10, 20) + 记录 20

作用

在 RR 下对范围做当前读时,严密防止"幻行"。即使是"插入刚好落到边界附近",也会被拦住。

面试常用答法:Next-Key Lock 用来防止在 RR 的当前读下发生幻读


五、等值查询 vs 范围查询的加锁差异

等值查询 + 唯一索引 -> 只加记录锁

sql
-- id 是主键/唯一索引
UPDATE t SET ... WHERE id = 20;
-- 一般只锁 id=20 这一条记录

范围查询 -> 触发 gap / next-key

sql
UPDATE t SET ... WHERE id > 10 AND id < 25;
-- 会锁范围内的记录 + 间隙
查询类型索引情况加锁范围
等值 + 唯一索引精确匹配记录锁(最小)
等值 + 普通索引可能多行匹配记录锁 + gap
范围查询走索引记录锁 + gap + next-key
无索引全表扫描锁大量行/范围(接近锁表)

六、InnoDB "表锁"是怎么来的

InnoDB 默认是行锁,但以下情况锁范围会很大:

  1. 没有合适索引 -> 执行计划扫描大量行,锁住更多记录/范围
  2. 查询条件无法走索引 -> 只能锁住更大范围
  3. 显式 LOCK TABLES -> 真正的表锁(一般不用)
  4. MDL(元数据锁) -> DDL 改表结构时发生,与业务行锁不同类

排查"锁等待"时,要看是在等行锁(record/gap/next-key)还是MDL


七、死锁

7.1 什么是死锁

两个事务彼此持有对方需要的锁,谁也等不到,只能回滚其中一个。

7.2 经典死锁场景

表有行 id=1id=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 怎么排查

sql
SHOW ENGINE INNODB STATUS;  -- 看死锁日志
  • 结合 performance_schema 查 lock wait
  • 结合事务 SQL、锁住的索引与范围分析

7.5 怎么避免

策略说明
固定资源获取顺序永远先更新 id 小的再更新大的
缩短事务时间减少持锁时间
走合适索引避免锁大量行/范围
减少单次锁的行数批量操作不要全量扫
失败重试死锁是常态化处理

八、完整加锁流程示意

事务 A 执行:

sql
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=22id=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",有时只锁一行,有时锁范围很多?

答案:锁的粒度取决于查询条件能不能高效利用索引

查询条件索引情况锁类型
等值 + 主键/唯一索引能精确定位记录锁(单行)
等值 + 普通索引可能多行记录锁 + 可能间隙锁
范围查询有索引记录锁 + 间隙锁 / 临键锁
没有索引 / 索引失效全表扫描锁大量行(接近锁表效果)

核心判断规则:

  1. 能精确定位(等值 + 唯一索引)→ 锁一行
  2. 范围查询 → 锁范围 + 间隙
  3. 非唯一索引 → 锁多行 + 可能锁间隙
  4. 没索引 / 索引失效 → 锁很多行,甚至扫全表

下一讲预告

第 7 讲:慢查询优化与性能调优

  • 怎么定位慢 SQL
  • 深分页为什么慢、怎么优化
  • count(*) 为什么有时慢
  • join 优化
  • 批量写入优化
  • 大事务问题

基于 VitePress 构建