跳到主要内容
约 11 分钟5 张卡

InnoDB 锁

InnoDB 通过索引记录锁和索引区间锁约束并发写入。本文讲解 MySQL 8.0 的锁类型、扫描范围和双会话验证。

1. 面试核心​

先记住这几条,再读正文
  • 行级锁类型。 record lock 锁定索引记录,gap lock 阻止间隙插入。next-key lock 组合记录与前方间隙。卡 · 记录锁
  • 唯一等值查找。 RR 下,完整唯一键等值锁定读命中已有记录时只加记录锁。未命中时,它锁住目标键所在间隙。卡 · 唯一键命中与未命中的双会话实验
  • 非唯一与范围查找。 RR 下,非唯一索引等值锁定读保护匹配记录及相邻间隙。范围锁定读按索引扫描路径加锁。卡 · 二级索引等值与范围锁实验
  • RC 的间隙锁边界。 RC 的普通搜索和索引扫描使用记录锁。外键与重复键检查仍可使用间隙锁。
  • 失败与重试。 innodb_rollback_on_timeout=OFF 时,锁超时回滚当前语句。死锁检测选中的受害事务整体回滚。卡 · 死锁、超时与重试

2. 记录锁​

记录锁作用于索引记录。事务通过二级索引更新一行时,会锁住访问到的二级索引记录。它还会锁住对应的聚簇索引记录。

UPDATE orders SET status = 'PAID' WHERE id = 100;

如果 id 是唯一主键,查询又精确命中一行,就只对该索引记录加锁。

3. 间隙锁​

间隙锁作用于相邻索引记录之间的开区间。它阻止其他事务在该区间插入新索引记录。

例如索引中已有 10 和 20,查询锁住 (10, 20) 的间隙。此时,其他事务插入 15 会等待。10 和 20 的更新是否等待,由端点记录上的锁决定。

不同事务的 gap lock 可以共存于同一间隙,它们共同抑制插入;gap lock 的 S/X 标记在兼容性上没有区别。

4. Next-key lock​

Next-key lock 是记录锁与该记录前方间隙锁的组合。在可重复读下,范围锁定读或更新常用 next-key lock。它覆盖扫描区间,防止其他事务在该范围插入新记录。

假设索引值为 10、20、30。next-key 锁按 (-∞, 10]、(10, 20]、(20, 30] 等区间组织。真实边界仍取决于查询条件与优化器选择的访问路径。

完整唯一索引的等值查找,若命中已有记录且确认唯一,只需记录锁。联合唯一索引的部分键查询按非唯一范围处理;唯一等值查询未命中时,RR 锁住目标值所在间隙以阻止插入。

5. 插入意向锁​

事务准备向某个间隙插入时,会先取得插入意向锁。多个事务可以并发插入同一间隙中的不同位置。若其他事务持有目标位置或间隙的不兼容锁,插入就需要等待。

插入意向锁属于间隙锁的一种,用来表达具体插入位置。

6. 意向锁​

InnoDB 在取得行级共享锁前,会在表级取得意向共享锁。取得行级排他锁前,则会在表级取得意向排他锁。表锁请求通过意向锁,能快速判断是否存在不兼容的行锁。因此,它可以省去遍历所有记录锁的步骤。

行记录上,S 与 S 兼容,S 与 X、X 与 X 不兼容;表级 IS/IX 表示已有或计划取得的行锁,它们彼此兼容。分析业务锁等待时,主要仍要找到实际的记录锁、间隙锁和持有事务。

7. 隔离级别与间隙锁​

在可重复读下,范围更新、删除和锁定读会锁扫描到的记录。它们使用 next-key lock 或边界 gap lock,也保护相应间隙。普通一致性读的可见版本由 MVCC 决定。

在读已提交下,普通搜索和索引扫描只锁匹配的索引记录;外键约束检查和重复键检查仍可使用间隙锁。减少间隙锁能提高并发。同一事务的两次查询也可以看到新提交的记录。

8. 扫描路径与锁范围​

SELECT *
FROM orders
WHERE tenant_id = 10 AND created_at >= '2026-08-01'
FOR UPDATE;

若存在 (tenant_id, created_at) 索引,InnoDB 可以锁住对应的连续索引范围。缺少合适索引时,执行计划可能采用全表扫描。锁范围也随扫描范围扩大,实际影响会远大于最终返回的行数。

过宽的访问路径会取得大量索引记录锁。为锁定读设计索引时,需要同时考虑查询性能和锁范围。

9. 锁等待的诊断证据​

排查锁等待时可以组合使用:

  • 使用 performance_schema.data_locks 查看当前持有或等待的锁。
  • 使用 performance_schema.data_lock_waits 查看阻塞关系。
  • 使用 SHOW ENGINE INNODB STATUS 查看最近死锁和事务信息。
  • 通过执行计划确认所用索引与预计扫描记录数。

先找到等待事务、阻塞事务和锁定索引。再检查对应的 SQL 与事务边界。在阻塞尚未解除时增加锁等待超时,会延长连接占用。

10. 唯一键命中与未命中的双会话实验​

在独立练习库初始化:

CREATE TABLE lock_demo (id INT PRIMARY KEY, value INT) ENGINE=InnoDB;
INSERT INTO lock_demo VALUES (10, 1), (20, 1);

会话 A 先执行:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM lock_demo WHERE id = 15 FOR UPDATE;

查询结果为空。在该数据布局下,15 位于 (10, 20) 间隙。会话 B 执行 INSERT INTO lock_demo VALUES (15, 1) 会等待 A 结束事务;A COMMIT 后 B 才能继续。若 A 改查已有主键 10,则保护的是 10 的记录,B 仍可插入 15。

每轮实验结束后提交或回滚两个会话,再恢复表数据。用 data_locks 与 data_lock_waits 对照锁模式和索引;换成二级索引、非唯一条件或 RC 时,结论要重新核对。

10.1 二级索引等值与范围锁实验​

以下实验适用于 MySQL 8.0 及以上。会话 A 使用 RR、升序扫描和 FOR UPDATE。它通过 FORCE INDEX 固定访问路径。每条查询单独开启事务。观察完成后执行 ROLLBACK,再进行下一条。

CREATE TABLE range_demo (
id INT PRIMARY KEY,
u INT NOT NULL,
k INT NOT NULL,
value INT,
UNIQUE KEY uk (u),
KEY nk (k)
) ENGINE=InnoDB;
INSERT INTO range_demo VALUES
(5,5,5,0), (10,10,10,0), (15,15,15,0), (20,20,20,0);

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM range_demo FORCE INDEX (nk) WHERE k=10 FOR UPDATE;
-- 保持事务打开,在另一会话查询 data_locks。
-- 观察结束后 ROLLBACK,再分别执行下表其他查询。

下表按索引列值表示区间。本例每个值只有一条记录。uk 与 nk 都是二级索引,叶子中的定位键还包含主键 id。

查询(省略相同 SELECT 前缀)访问索引上的锁聚簇索引上的锁
FORCE INDEX (nk) WHERE k=10 FOR UPDATE10 上的 next-key (5,10],15 前的 gap (10,15)主键 10 的记录锁
FORCE INDEX (uk) WHERE u>5 AND u<15 FOR UPDATE10、15 上的 next-key,即 (5,10]、(10,15]主键 10 的记录锁
FORCE INDEX (nk) WHERE k>5 AND k<15 FOR UPDATE10、15 上的 next-key,即 (5,10]、(10,15]主键 10 的记录锁
FORCE INDEX (PRIMARY) WHERE id>5 AND id<15 FOR UPDATE10 上的 next-key (5,10],15 前的 gap (10,15)同一棵主键索引,无额外二级索引锁

非唯一等值查找必须继续确认等值组的结束位置。因此,它会保护末个匹配项之后的间隙。若 k=10 有多条记录,锁范围应按 (k,id) 的完整顺序推导。范围包括所有匹配记录和组内间隙。两侧扫描到的间隙也需要计入。

唯一索引只有完整唯一键等值命中才适用记录锁优化;范围查询仍需间隙保护。本例中,15 不在二级索引范围查询的返回结果中。但扫描仍在 15 上保留 next-key 锁。主键范围扫描能在该上界只保留 gap lock。实际锁会受上界是否命中、扫描方向、索引类型和执行计划影响。任一条件改变时,都应重新观察锁。

10.2 data_locks 与阻塞验证​

在会话 B 查询锁,DATABASE() 要与实验表所在数据库一致:

SELECT INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_DATA, LOCK_STATUS
FROM performance_schema.data_locks
WHERE OBJECT_SCHEMA = DATABASE() AND OBJECT_NAME = 'range_demo'
ORDER BY INDEX_NAME, LOCK_DATA;
  • LOCK_TYPE=TABLE、LOCK_MODE=IX 是表级意向锁。
  • 对本例的 RECORD 锁,X,REC_NOT_GAP 表示纯记录锁。X,GAP 表示 LOCK_DATA 所列记录前的间隙。X 表示 next-key 锁。
  • 非唯一等值查询显示三项锁:nk / X / 10, 10、nk / X,GAP / 15, 15、PRIMARY / X,REC_NOT_GAP / 10。它们分别对应上表三处保护。

保持 A 的事务打开。B 执行 SET SESSION innodb_lock_wait_timeout=1; START TRANSACTION; INSERT INTO range_demo VALUES (12,12,12,0);。由于 (10,15) 受保护,B 应超时,随后执行 ROLLBACK。另起一轮,B 执行 SELECT * FROM range_demo FORCE INDEX (nk) WHERE k=15 FOR UPDATE。A 持有非唯一等值查询的锁时,B 可以锁定 15。A 持有非唯一范围查询的锁时,B 等待。验证唯一二级索引范围时,B 改用 FORCE INDEX (uk) WHERE u=15。观察活动等待可在第三个会话查询 performance_schema.data_lock_waits。

每轮最后回滚 A 与 B;用 EXPLAIN 确认访问索引。再对照锁模式、完整索引键和冲突操作,结合三项验证区间。

11. 死锁、超时与重试​

两个事务按相反顺序占有并请求记录锁,会形成循环等待。例如 A 先锁 10 再等 20,B 先锁 20 再等 10。普通等待可随持锁方释放而结束;死锁需要打破等待环。InnoDB 默认启用死锁检测,选择受害事务回滚;innodb_deadlock_detect=OFF 时依赖 innodb_lock_wait_timeout 超时处理。

死锁检测会回滚整个受害事务;innodb_rollback_on_timeout=OFF 时,锁等待超时只回滚当前语句。ON 时,超时会回滚整个事务。完整失败语义见 数据库事务。需要共同成立的业务操作应结束失败事务后整体重试。

统一访问顺序、缩短事务可以减少循环等待。为过滤条件建合适索引也有同样作用。有界退避重试应放在新事务中,并以幂等标识约束副作用。

12. NOWAIT 与 SKIP LOCKED​

FOR UPDATE NOWAIT 遇到需要等待的行锁时直接报错;FOR UPDATE SKIP LOCKED 从结果中跳过被锁定的行。它们分别提供立即失败和跳过忙碌记录的处理方式。

多个消费者抢取任务时,可以用 SKIP LOCKED 跳过忙碌记录。选取与状态更新要在同一事务中完成。提交后再处理耗时任务,并另行设计失败重试与租约。SKIP LOCKED 返回的集合会跳过被锁行。因此,使用它的任务领取等场景需要允许结果不完整。

这些选项作用于行级锁等待,元数据锁等其他等待仍需单独处理。

13. 常见问题​

13.1 普通 SELECT 与行锁​

在 RC/RR 下,InnoDB 的普通一致性读通过 MVCC 读取可见版本;SERIALIZABLE 且 autocommit=0 时,普通 SELECT 隐式转为共享锁定读。SELECT ... FOR SHARE、SELECT ... FOR UPDATE 属于锁定读。它们与更新、删除操作都会根据访问范围加锁。

13.2 行锁的粒度与数量​

14 · 面试题卡片​

14.1 InnoDB 有哪些行级锁,如何判断一条 SQL 的加锁范围​

出现公司:美团

考察重点

  • 理解record、gap、next-key 与 insert intention lock。
  • 理解唯一等值查询和范围查询的差异。
  • 理解隔离级别、索引访问路径与实际锁范围。

相关内容:记录锁、间隙锁、Next-key lock、扫描路径与锁范围。

参考回答

InnoDB 的记录锁作用于索引记录。

gap lock 阻止在索引间隙中插入。next-key lock 是记录锁与前方间隙锁的组合。插入前还会使用 insert intention lock,表级意向锁用于协调表锁与行锁。

判断加锁范围要看隔离级别和执行计划。RR 下范围更新和锁定读按扫描路径使用 next-key lock 或边界 gap lock;完整唯一索引精确查找命中时只需记录锁,未命中时锁目标间隙。缺少合适索引时,访问路径会过宽。语句因此需要扫描并锁住大量索引记录。排查时应结合 data_locks、阻塞关系和实际执行计划。

14.2 唯一索引等值查询没有找到记录,还会阻止其他事务插入吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点

  • 理解命中记录与不存在记录的差异。
  • 理解RR 间隙锁与双会话验证。

相关内容:唯一键命中与未命中的双会话实验。

参考回答

会。

RR 下唯一键等值锁定读未命中时,锁住目标所在间隙。已有 10、20 时锁定查询 15,另一个事务插入该间隙会等待;完整唯一键查询命中 10 则只锁记录。还要确认事务未提交、实际索引和隔离级别。

14.3 死锁和锁等待超时有什么区别,失败后怎样重试?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点

  • 理解等待环与超时阈值。
  • 理解回滚范围与新事务重试。

相关内容:死锁、超时与重试、锁等待的诊断证据。

参考回答

死锁是循环等待,锁等待超时是等待超过 innodb_lock_wait_timeout;死锁检测回滚受害事务,innodb_rollback_on_timeout=OFF 时超时只回滚当前语句。

先记录阻塞关系并确认事务状态,再回滚或恢复业务单元;整体重试应开启新事务,限制次数并处理重复副作用。

14.4 FOR UPDATE 的 NOWAIT 和 SKIP LOCKED 分别适合什么场景?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点

  • 理解立即失败与跳过记录。
  • 理解任务领取与不完整结果集。

相关内容:NOWAIT 与 SKIP LOCKED。

参考回答

NOWAIT 把行锁等待变为立即错误,便于上层快速失败或重试。

SKIP LOCKED 略过已锁记录,适合并发领取不同任务。返回结果会遗漏本次忙碌的记录。选取、标记任务需在同一事务内;后续执行失败仍需租约或补偿。任务唯一标识和幂等处理用于约束重复副作用。

14.5 非唯一等值、唯一范围与非唯一范围查询分别锁哪些区间?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:二级索引扫描、边界记录、间隙与 data_locks 验证。

相关内容:二级索引等值与范围锁实验、data_locks 与阻塞验证。

参考回答

RR 下,非唯一等值查找保护匹配记录及相邻间隙;唯一与非唯一范围查找都需要间隙保护,具体边界取决于扫描路径。

以正文 MySQL 8.0 实验为例,u、k 的值均为 5、10、15、20,每个值只有一条记录,并固定升序二级索引扫描:

  • k=10 保护 (5,10] 和 (10,15)。
  • u>5 AND u<15 与 k>5 AND k<15 保护 (5,10]、(10,15]。
  • 这些查询还给返回行的主键 10 加记录锁;改为主键范围扫描时,上界 15 在该实验中只保留 gap lock。

完整唯一键等值命中才适用单记录优化。数据分布、扫描方向或索引改变后,应结合执行计划与 data_locks 重新核对。

15. 参考资料​