📖 本篇简介:锁与 MVCC 是 MySQL 并发控制的两大基石,也是面试高频考点。本篇以表格化形式汇总锁类型、行锁表锁对比、三种锁算法(record/gap/next-key)、MVCC 原理、死锁排查、锁等待排查等内容。所有示例基于 MySQL 8.0 语法。读完你能快速判断一条 SQL 会加什么锁、会不会死锁、怎么排查锁问题。
锁和 MVCC 是一对搭档:读用 MVCC(不加锁),写用锁。理解了这俩,MySQL 并发模型就通了一大半。
面试问锁,十有八九会问到 next-key lock、间隙锁、MVCC 可见性判断这三块。本篇把这些整理成表格,考前过一遍。
目录
- 锁类型速查表
- 行锁与表锁对比
- 三种行锁算法
- MVCC 与锁的关系
- MVCC 实现细节速记
- 死锁排查命令
- 锁等待排查命令
- 锁相关参数速查
- 面试高频问答
- 常见误区澄清
- 隔离级别与锁对比
- 加锁规则速记
- 常见锁问题场景
- 死锁案例速记
- 自测:锁与MVCC速查
锁类型速查表
| 锁类型 | 说明 | 加锁时机 |
|---|---|---|
| 共享锁 S | 读锁,可共存 | SELECT ... LOCK IN SHARE MODE |
| 排他锁 X | 写锁,互斥 | UPDATE/DELETE/SELECT ... FOR UPDATE |
| 意向锁 IS/IX | 表级锁,表明意图 | 加行锁前自动加 |
| 记录锁 | 锁单行 | 唯一索引等值查询 |
| 间隙锁 | 锁范围间隙 | 范围查询,防插入 |
| Next-Key Lock | 记录+间隙 | RR 默认,防幻读 |
| 临键锁 | next-key 同义词 | |
| 插入意向锁 | INSERT 前等待 | INSERT 时 |
| AUTO-INC 锁 | 自增锁 | INSERT 自增列 |
| Metadata Lock | 元数据锁 | DDL 时 |
| 全局锁 | FTWRL,整库只读 | 备份时 |
| 命名锁 | RENAME TABLE 时 | 表名变更 |
| 保护锁 | 防止表被 DROP | DDL/DML 冲突 |
| 全局读锁 | FTWRL,整库只读 | 逻辑备份 |
| 备份锁 | 8.0 在线备份锁 | 不阻塞 DML |
| 命名锁 | RENAME TABLE 时 | 表名变更 |
| 保护锁 | 防止表被 DROP | DDL/DML 冲突 |
锁按粒度分:全局锁、表锁、行锁。按性质分:共享锁(S)、排他锁(X)。InnoDB 默认用行锁,并发高。
意向锁(IS/IX)是表级锁,加行锁前自动加,目的是让表锁快速判断有没有行锁,不用逐行查。
-- 显式加锁示例(本示例基于 MySQL 8.0)
-- 共享锁
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;
-- 排他锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 现在就跳过大事务,不用等(8.0)
SELECT * FROM account WHERE id = 1 FOR UPDATE NOWAIT;
行锁与表锁对比
| 对比项 | 行锁 | 表锁 |
|---|---|---|
| 粒度 | 行 | 整张表 |
| 并发 | 高 | 低 |
| 开销 | 大(加锁慢) | 小(加锁快) |
| 存储 | 锁对象多 | 锁对象少 |
| 适用引擎 | InnoDB | MyISAM / Memory |
| 死锁 | 可能 | 不可能 |
| 适用 | InnoDB 并发写 | MyISAM / DDL |
| 什么时候触发 | WHERE 走索引 | 不走索引、DDL |
| 释放时机 | 事务提交/回滚 | 语句结束 |
| 并发度 | 高 | 低 |
| 开销 | 大 | 小 |
表锁粒度大,并发低,但加锁快、开销小。行锁粒度小,并发高,但加锁慢、开销大。MyISAM 只用表锁,InnoDB 默认行锁。
关键结论:InnoDB 行锁是加在索引上的。如果 WHERE 不走索引,会退化成表锁(实际是全表加记录锁,效果等于表锁)。
三种行锁算法
| 算法 | 锁定范围 | 触发条件 |
|---|---|---|
| Record Lock | 单条记录 | 唯一索引等值查询,记录存在 |
| Gap Lock | 间隙(开区间) | 范围查询,防插入 |
| Next-Key Lock | 记录 + 前面间隙(左开右闭) | RR 默认,范围查询 |
| 插入意向锁 | INSERT 时等待间隙锁 | INSERT 语句 |
InnoDB 行锁有三种算法,理解了这三种就能判断一条 SQL 会锁哪些行。
-- 假设有索引 id: 1, 5, 10
-- SELECT * FROM t WHERE id = 5 FOR UPDATE
-- → Record Lock 锁 id=5 这一行
-- SELECT * FROM t WHERE id > 3 AND id < 8 FOR UPDATE
-- → Next-Key Lock 锁 (3, 5] 和 (5, 10) 之间的间隙
-- → 其他事务无法在这个范围 INSERT
间隙锁只在 RR 级别有。RC 级别没有间隙锁,所以 RC 会幻读。
插入意向锁:INSERT 时如果碰到间隙锁,要等。多个 INSERT 插入不同位置不互斥。
MVCC 与锁的关系
| 机制 | 读 | 写 |
|---|---|---|
| MVCC | 快照读,不加锁 | 不涉及 |
| 当前读 | 加锁读(FOR UPDATE 等) | 加 X 锁 |
| 锁 | 不涉及 | 保证写写互斥 |
| 阻塞 | 不阻塞写 | 读写不互相阻塞 |
MVCC 让读不加锁,锁只管写。这样读写不互相阻塞,并发性能高。
快照读 vs 当前读:
- 快照读:普通 SELECT,走 MVCC,读历史版本,不加锁
- 当前读:SELECT ... FOR UPDATE、UPDATE、DELETE,读最新版本,加锁
-- 快照读(本示例基于 MySQL 8.0)
SELECT * FROM account WHERE id = 1; -- MVCC,不加锁
-- 当前读
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 加 X 锁
UPDATE account SET balance = balance - 100 WHERE id = 1; -- 加 X 锁
关键:UPDATE 隐含当前读,先读最新版本再加锁。如果一个事务里先快照读再 UPDATE,可能看不到其他事务刚提交的修改,UPDATE 时才发现变了。
MVCC 实现细节速记
MVCC 是 InnoDB 高并发的核心,面试必问。理解它要拆开看三个组件。
MVCC = 隐藏列 + undo log 版本链 + ReadView。
| 组件 | 作用 |
|---|---|
| DB_TRX_ID | 最近修改这行的事务 ID |
| DB_ROLL_PTR | 回滚指针,指向上一个版本 undo log |
| undo log | 保存历史版本,形成版本链 |
| ReadView | 读视图,判断哪个版本可见 |
| 版本链 | undo log 串起来,找可见版本 |
ReadView 四要素:
- m_ids:当前活跃事务 ID 列表
- min_trx_id:最小活跃事务 ID
- max_trx_id:下一个将分配的事务 ID
- creator_trx_id:创建这个 ReadView 的事务 ID
可见性算法:版本的 trx_id 如果在 m_ids 里 → 不可见;< min_trx_id → 可见;> max_trx_id → 不可见;等于 creator → 可见。
RC vs RR 的 ReadView:
- RC:每条 SELECT 都生成新 ReadView,能看到其他事务刚提交的
- RR:事务第一条 SELECT 生成 ReadView,之后复用,可重复读
长事务危害:长事务持有 ReadView 很久,undo log purge 不掉,表越来越大。
死锁排查命令
死锁是并发写的常见问题。InnoDB 自动检测死锁,回滚代价小的事务。但要定位根因,得看死锁日志。
-- 查看最近死锁日志(本示例基于 MySQL 8.0)
SHOW ENGINE INNODB STATUS;
-- 找 LATEST DETECTED DEADLOCK 段
-- 查看当前锁等待
SELECT * FROM information_schema.innodb_lock_waits;
-- 查看当前持锁事务
SELECT * FROM information_schema.innodb_trx;
-- 杀死阻塞事务
KILL <trx_mysql_thread_id>;
-- KILL 后事务自动回滚,释放锁
死锁四个必要条件:互斥、持有并等待、不可剥夺、循环等待。破坏任意一个就能避免死锁。
死锁发生后:InnoDB 自动检测,回滚代价小的那个事务(undo log 少的),另一个继续。业务代码要捕获死锁异常并重试。
死锁日志怎么看:SHOW ENGINE INNODB STATUS 里 LATEST DETECTED DEADLOCK 段,会打印两个事务分别等什么锁、持什么锁。
锁等待排查命令
锁等待是业务卡住的常见原因。排查思路:先看谁在等锁,再看谁持着锁不放。
-- 查看锁等待超时时间(本示例基于 MySQL 8.0)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 查看当前谁在等锁
SELECT
r.trx_wait_started AS wait_time,
b.trx_mysql_thread_id AS block_thread,
r.trx_mysql_thread_id AS wait_thread
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
-- 查到 block_thread 后 KILL 掉它,等待方就能继续
锁等待超时:默认 50 秒,超时后报 ERROR 1205。生产环境建议改小到 5~10 秒,快速失败别占着。
排查思路:先看 innodb_trx 有没有长事务,再看 innodb_lock_waits 谁等谁,找到阻塞源 KILL 掉。
常见阻塞源:忘记提交的事务、大事务、DDL 等 MDL 锁、备份拿全局锁。
锁相关参数速查
| 参数 | 默认值 | 说明 |
|---|---|---|
| innodb_lock_wait_timeout | 50 秒 | 行锁等待超时 |
| innodb_deadlock_detect | ON | 死锁检测 |
| innodb_deadlock_log_prioritize | | 死锁日志优先级 |
| transaction_isolation | REPEATABLE-READ | 隔离级别 |
| innodb_autoinc_lock_mode | 1 | 自增锁模式 |
| innodb_print_all_deadlocks | OFF | 死锁打印到 error log |
| innodb_status_output_locks | OFF | 锁状态输出 |
| lock_wait_timeout | 31536000 | 表锁等待超时 |
| innodb_deadlock_detect | ON | 死锁检测开关 |
| innodb_thread_concurrency | 0 | 并发线程数限制 |
| innodb_rollback_on_timeout | OFF | 超时是否回滚整个事务 |
| innodb_print_all_deadlocks | OFF | 死锁打印到 error log |
锁相关参数大部分用默认值就行,但有几个建议根据业务调优。
面试高频问答
| 问题 | 一句话答案 |
|---|---|
锁与 MVCC 面试高频题,每题一句话答案,考前过一遍。
| 问题 | 一句话答案 |
|---|---|
| InnoDB 行锁加在哪 | 加在索引记录上,不走索引就锁全表 |
| 什么是 next-key lock | 记录锁+间隙锁,左开右闭区间 |
| RC 为什么没有间隙锁 | RC 不防幻读,不需要间隙锁 |
| 死锁怎么产生 | 两事务循环等锁 |
| 怎么避免死锁 | 统一加锁顺序、缩短事务、走索引 |
| MVCC 是什么 | 多版本并发控制,读写不阻塞 |
| 快照读和当前读区别 | 快照读读历史版本不加锁,当前读读最新加锁 |
| 什么是意向锁 | 表级锁,表明要加行锁的意图 |
| 表锁什么时候触发 | DDL、不走索引的 DML |
| 自增锁是什么 | 保证自增列连续,AUTO-INC |
| RC 和 RR 怎么选 | 互联网一般用 RC,金融强一致用 RR |
| 间隙锁什么时候有 | RR 级别范围查询 |
| 怎么排查死锁 | SHOW ENGINE INNODB STATUS 看日志 |
| 死锁了怎么办 | 自动回滚代价小的事务,业务重试 |
| 锁等待超时怎么办 | KILL 阻塞者,缩短事务 |
| 什么是元数据锁 | DDL 时保护表结构,防并发 DML |
| 为什么慢 SQL 锁全表 | WHERE 不走索引,全表加行锁 |
| 什么是插入意向锁 | INSERT 前等间隙锁的锁 |
| 自增锁有几种模式 | 0/1/2,2 是 interleaved 性能最好 |
| MDL 锁什么时候有 | DDL 操作时,保护表结构 |
| 为什么慢 SQL 会锁全表 | WHERE 不走索引,全表加行锁 |
| 死锁了业务怎么办 | 捕获异常重试 |
常见误区澄清
| 误区 | 正解 |
|---|---|
| 加了行锁就不会死锁 | 行锁之间也会循环等待 |
| MVCC 完全不用锁 | 写操作还是要加 X 锁 |
| 间隙锁没用 | 防幻读,RR 必须有 |
| RC 没有锁 | RC 有记录锁,只是没间隙锁 |
| 死锁是 bug | 死锁是并发的正常现象 |
| 锁越多越安全 | 锁多了并发低,要平衡 |
| SELECT 就不加锁 | SELECT FOR UPDATE 加 X 锁 |
| 事务越长越安全 | 长事务持锁久,阻塞多 |
| 间隙锁只锁索引 | 间隙锁在索引记录之间 |
| 死锁一定要避免 | 死锁是正常现象,重试就行 |
| MVCC 解决一切 | MVCC 只管读,写还是要锁 |
隔离级别与锁对比
不同隔离级别加锁行为不同,这也是面试常考。
| 隔离级别 | 快照读 | 当前读 | 间隙锁 |
|---|---|---|---|
| READ UNCOMMITTED | 读最新,不加锁 | 加锁 | 有 |
| READ COMMITTED | 每次 SELECT 新 ReadView | 加记录锁,无 gap | 无 |
| REPEATABLE READ | 第一次 SELECT 生成 ReadView | next-key lock | 有 |
| SERIALIZABLE | 所有读都升级为共享锁 | 加锁 | 有 |
| READ UNCOMMITTED | 读最新,不加锁 | 加锁 | 有 |
关键区别:RC 没有间隙锁,所以并发更高,但会幻读。RR 有间隙锁,防幻读,但并发稍低。
生产选型建议:互联网业务(订单、用户、商品)一般用 RC,并发高够用;金融、账务类用 RR,强一致。
加锁规则速记
MySQL 8.0 InnoDB 加锁规则(RR 级别):
加锁规则是面试难点,记住这几条原则就能推导出一条 SQL 会加什么锁。
| 规则 | 说明 |
|---|---|
| 原则 1 | 加锁的基本单位是 next-key lock(左开右闭) |
| 原则 2 | 查找过程中访问到的对象才会加锁 |
| 优化 1 | 索引上的等值查询,唯一索引命中,退化为记录锁 |
| 优化 2 | 索引上的等值查询,向右遍历最后一个不满足,next-key 退化为 gap |
| 等值查询未命中 | next-key 退化为 gap lock |
| 范围查询 | 访问到不满足条件的第一个值为止 |
| 唯一索引等值未命中 | next-key 退化为 gap lock |
| 非唯一索引等值 | next-key lock,不退化为记录锁 |
| 多列联合索引 | 按索引顺序逐列加锁 |
| 唯一索引范围 | 退化为记录锁 |
| 非唯一索引范围 | next-key lock |
| 唯一索引等值未命中 | 退化为 gap lock |
-- 加锁示例(本示例基于 MySQL 8.0,索引 id: 5, 10, 15)
-- SELECT * FROM t WHERE id = 10 FOR UPDATE
-- → 唯一索引等值命中 → Record Lock 只锁 id=10
-- SELECT * FROM t WHERE id > 8 AND id < 12 FOR UPDATE
-- → Next-Key Lock 锁 (5, 10] 和 (10, 15)
-- → 其他事务无法在这个范围 INSERT 新行
常见锁问题场景
线上锁问题常见场景速查,遇到对照着查。
| 场景 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 慢查询锁等待 | 业务卡住 | 慢 SQL 不走索引锁全表 | 加索引、优化 SQL |
| 批量更新死锁 | 偶发 deadlock | 加锁顺序不一致 | 统一排序后更新 |
| 间隙锁锁范围 | INSERT 被阻塞 | RR 范围查询加间隙锁 | 改 RC、缩小范围 |
| 大事务持锁 | 锁等待超时 | 事务太长 | 拆分事务 |
| DDL 卡住 | ALTER 等锁 | 元数据锁等待 | 低峰期、pt-osc |
| 备份锁 | mysqldump 卡住 | FTWRL 全局锁 | --single-transaction |
| 间隙锁阻塞 | INSERT 报锁等待 | RR 间隙锁 | 改 RC、缩小范围 |
| 大事务阻塞复制 | 从库回放慢 | 大事务 | 拆分事务 |
| 长事务占 undo | undo purge 不掉 | 事务太长 | 缩短事务 |
| 备份锁阻塞 | 备份时业务慢 | FTWRL 全局锁 | --single-transaction |
| 间隙锁阻塞插入 | INSERT 报锁等待 | RR 间隙锁 | 改 RC、缩小范围 |
死锁案例速记
死锁最经典的场景就是两个事务交叉更新,画一遍就懂。
-- 死锁典型场景(本示例基于 MySQL 8.0)
-- 事务 A:
BEGIN;
UPDATE account SET balance=balance-100 WHERE id=1; -- 锁 id=1
UPDATE account SET balance=balance+100 WHERE id=2; -- 等 id=2 锁
-- 事务 B:
BEGIN;
UPDATE account SET balance=balance-100 WHERE id=2; -- 锁 id=2
UPDATE account SET balance=balance+100 WHERE id=1; -- 等 id=1 锁
-- 死锁!InnoDB 检测到后回滚其中一个事务
避免方法:两个事务都按 id 升序更新,就不会循环等待。
其他避免死锁的方法:
1. 统一加锁顺序(最有效)
2. 大事务拆小,缩短持锁时间
3. 保证 UPDATE/DELETE 走索引,减少锁范围
4. 降低隔离级别(RC 无间隙锁)
业务代码捕获死锁异常后要重试,重试几次成功就过了。
自测:锁与MVCC速查
下面 10 道题覆盖锁与 MVCC 核心考点,先自己想答案,再展开参考答案。
题 1:InnoDB 行锁加在什么上?
<details>
<summary>参考答案</summary>
加在索引记录上。如果 WHERE 条件不走索引,会全表扫描加锁,效果等于表锁。所以慢 SQL 不仅慢,还会锁全表。
</details>
题 2:next-key lock 是什么?
<details>
<summary>参考答案</summary>
记录锁 + 间隙锁,锁定一个左开右闭区间。RR 级别默认用 next-key lock 防止幻读。比如索引有 1,5,10,查 id>3 会锁 (3,5] 和 (5,10) 的间隙。
</details>
题 3:快照读和当前读的区别?
<details>
<summary>参考答案</summary>
快照读是普通 SELECT,走 MVCC 读历史版本,不加锁;当前读是 SELECT FOR UPDATE、UPDATE、DELETE,读最新版本,加 X 锁。
</details>
题 4:死锁的四个必要条件?
<details>
<summary>参考答案</summary>
互斥、持有并等待、不可剥夺、循环等待。破坏任意一个就能避免死锁。实际中最常用的是统一加锁顺序,破坏循环等待。
</details>
题 5:RC 级别为什么不会幻读?
<details>
<summary>参考答案</summary>
RC 其实会幻读。RC 没有间隙锁,范围查询时其他事务可以插入新行,下次查就能看到。InnoDB 在 RR 级别才通过 next-key lock 解决幻读。
</details>
题 6:什么是意向锁?
<details>
<summary>参考答案</summary>
意向锁是表级锁,加行锁前先在表上加意向锁(IS/IX)。作用是:其他事务想加表锁时,快速判断表里有没有行锁,不用逐行检查。意向锁之间不互斥。
</details>
题 7:为什么慢 SQL 会锁全表?
<details>
<summary>参考答案</summary>
InnoDB 行锁加在索引上。如果 UPDATE/DELETE 的 WHERE 不走索引,会全表扫描,每一行都加 X 锁,效果等于表锁。其他事务想写这张表就全堵了。
</details>
题 8:间隙锁什么时候会阻塞 INSERT?
<details>
<summary>参考答案</summary>
RR 级别范围查询加 next-key lock(含间隙)。如果两个事务的间隙锁范围重叠,第二个事务 INSERT 会被阻塞。RC 级别没有间隙锁,不会阻塞。
</details>
题 9:怎么排查锁等待?
<details>
<summary>参考答案</summary>
查 information_schema.innodb_lock_waits 看谁等谁,查 innodb_trx 看事务详情,找到阻塞者的 thread id,KILL 掉。先看是不是有大事务没提交。
</details>
题 10:死锁检测开还是关?
<details>
<summary>参考答案</summary>
默认开。并发高时死锁检测本身耗 CPU(要遍历等待图)。并发极高时可以关,靠锁等待超时兜底。一般业务开着就行。
</details>
题 11:为什么 RC 级别并发更高?
<details>
<summary>参考答案</summary>
RC 没有间隙锁,范围查询不会锁间隙,其他事务可以随便插入。而且 RC 每次 SELECT 生成新 ReadView,锁的粒度更小。代价是会幻读。
</details>
题 12:什么是元数据锁(MDL)?
<details>
<summary>参考答案</summary>
DDL 操作时加的表级锁,保护表结构。如果有长事务在跑,DDL 会等 MDL 锁,后面的 DML 也会被堵住。所以 ALTER 表要低峰期做。
</details>
题 13:什么是插入意向锁?
<details>
<summary>参考答案</summary>
INSERT 语句在插入前,如果插入位置有间隙锁,要等待。插入意向锁是一种特殊的间隙锁,多个事务插入不同位置不互斥。
</details>
题 14:为什么事务里不要做 RPC?
<details>
<summary>参考答案</summary>
RPC 很慢,事务里做 RPC 会拉长事务持锁时间,导致其他事务等锁。应该把 RPC 移到事务外面,事务里只做数据库操作。
</details>
题 15:什么是全局读锁?
<details>
<summary>参考答案</summary>
FTWRL(FLUSH TABLES WITH READ LOCK),整库只读,所有写操作阻塞。逻辑备份时用。推荐用 --single-transaction 代替,不阻塞写。
</details>
自测评分:14 道题答对 12 道以上,锁这块面试基本稳了;答对 7~11 道,建议把 next-key lock 和 MVCC 可见性再画一遍;答对 7 道以下,先把加锁规则背熟。
使用建议:遇到锁问题,第一反应是查 innodb_trx 和 innodb_lock_waits,不要猜。
面试前重点过:next-key lock 三种算法、MVCC 可见性判断、死锁四个必要条件。这三块是必考题。
线上遇到锁问题,按这个顺序查:1)innodb_trx 看长事务;2)innodb_lock_waits 看谁等谁;3)KILL 阻塞者;4)复盘根因。
(本章完)
—— END ——
下一篇见。
祝你面试顺利,线上无锁等待。
(完)
—— END ——
下一篇见。
下一篇预告:第 79 章 速查卡:主从与高可用
文章回复
0 条公开回复