目录
[1. 并发问题与隔离级别](#1. 并发问题与隔离级别)
[1.1 三大并发问题](#1.1 三大并发问题)
[1.2 四种隔离级别](#1.2 四种隔离级别)
[2. MVCC 多版本并发控制(重点)](#2. MVCC 多版本并发控制(重点))
[2.1 核心概念](#2.1 核心概念)
[2.2 版本链的形成](#2.2 版本链的形成)
[2.3 ReadView 可见性判断规则](#2.3 ReadView 可见性判断规则)
[2.4 RC vs RR 的 ReadView 区别(核心)](#2.4 RC vs RR 的 ReadView 区别(核心))
[2.5 MVCC实践示例](#2.5 MVCC实践示例)
[3. 幻读解决方案与Next-Key Lock](#3. 幻读解决方案与Next-Key Lock)
[3.1 幻读的典型场景(RR级别)](#3.1 幻读的典型场景(RR级别))
[3.2 Next-Key Lock 详解](#3.2 Next-Key Lock 详解)
[3.3 快照读与当前读的行为差异](#3.3 快照读与当前读的行为差异)
[4. 面试实战](#4. 面试实战)
[4.1 高频问题汇总](#4.1 高频问题汇总)
[4.2 追问清单与深入分析](#4.2 追问清单与深入分析)
1. 并发问题与隔离级别
当多个事务并发执行时,会产生各种数据不一致问题。MySQL通过四种隔离级别来解决这些问题。
1.1 三大并发问题
| 问题 | 描述 | 示例 |
|---|---|---|
| 脏读 | 读到其他事务未提交的数据 | A修改数据未提交,B读到修改后的值,A回滚,B读到的是无效数据 |
| 不可重复读 | 同一事务内两次读取同一数据结果不同 | A读取数据后,B修改并提交,A再次读取得到不同值 |
| 幻读 | 同一事务内两次查询结果集数量不同 | A查询记录数为N,B插入新记录并提交,A再次查询记录数为N+1 |
1.2 四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| 读未提交 READ UNCOMMITTED | ✓ | ✓ | ✓ | 最低级别,性能最好 |
| 读已提交 READ COMMITTED (RC) | ✗ | ✓ | ✓ | Oracle默认 |
| 可重复读 REPEATABLE READ (RR) | ✗ | ✗ | ✓* | MySQL默认 |
| 串行化 SERIALIZABLE | ✗ | ✗ | ✗ | 最高级别,性能最差 |
为什么MySQL选择RR作为默认级别?
-
RR级别解决了脏读和不可重复读问题
-
InnoDB通过MVCC+Next-Key Lock(行锁+间隙锁)在很大程度上避免了幻读
-
在互联网业务中,RR级别兼顾了数据一致性和并发性能
RC vs RR的选择:
-
阿里规范推荐RC + 行锁,因为RR的Next-Key Lock容易产生死锁,且高并发下RC性能更好
-
金融/对账类业务可能用RR,避免不可重复读导致数据不一致
1.3三种并发操作分类
在RR隔离级别下,任意两个事务并发执行时,按各自的读写角色归为三种场景:
| 并发场景 | 事务A | 事务B | 冲突类型 | 解决机制 |
|---|---|---|---|---|
| 读读并发 | 读 | 读 | 无冲突 | 天然支持 |
| 读写并发 | 读 | 写 | 读可能看到写的结果 | 通过MVCC实现 |
| 写写并发 | 写 | 写 | 脏写、更新丢失 | 加锁保证串行执行 |
2. MVCC 多版本并发控制(重点)
MVCC (Multi-Version Concurrency Control) 是InnoDB实现高并发的关键技术,它让读写操作不互相阻塞,显著提高了数据库并发性能。
2.1 核心概念
-
快照读 :普通
SELECT语句,通过MVCC读取历史版本,完全避免幻读 -
当前读 :
SELECT ... FOR UPDATE、UPDATE、DELETE,读取最新版本,通过Next-Key Lock(行锁+间隙锁)几乎避免幻读
2.2 版本链的形成
InnoDB为每行数据添加了三个隐藏列:
| 隐藏列 | 说明 |
|---|---|
DB_TRX_ID |
最近修改的事务ID(6字节) |
DB_ROLL_PTR |
回滚指针,指向Undo Log(7字节) |
DB_ROW_ID |
隐含的自增ID(6字节,如果没有主键则作为聚簇索引) |
通过DB_ROLL_PTR指针,可以将同一行数据的多个版本串联成一个版本链,每个版本对应一个Undo Log记录。
2.3 ReadView 可见性判断规则
ReadView是MVCC实现的核心数据结构,包含以下关键字段:
| 字段 | 说明 |
|---|---|
m_ids |
创建ReadView时,当前活跃(未提交)的事务ID列表 |
up_limit_id |
m_ids中的最小值 |
low_limit_id |
下一个待分配的事务ID(即当前最大事务ID + 1) |
creator_trx_id |
创建该ReadView的事务ID |

可见性判断流程:
-
规则1:若数据版本的事务ID < up_limit_id,说明该版本在ReadView创建前已提交,可见
-
规则2:若事务ID ≥ low_limit_id,说明该版本在ReadView创建后生成,不可见
-
规则3:若事务ID在m_ids中,说明该版本由未提交事务生成,不可见
-
规则4:若事务ID在up_limit_id和low_limit_id之间且不在m_ids中,说明该版本已提交,可见
2.4 RC vs RR 的 ReadView 区别(核心)
这是理解两种隔离级别差异的关键:
| 隔离级别 | ReadView生成时机 | 结果 |
|---|---|---|
| RC | 每次SELECT都生成新的ReadView | 能读到其他事务已提交的最新数据 |
| RR | 事务开始时生成一次,整个事务复用同一个 | 多次读取结果一致(可重复读) |
这是不可重复读的根本原因! RC级别每次SELECT都会生成新的ReadView,所以能看到其他事务提交的最新数据;而RR级别只在事务开始时生成一次ReadView,所以多次读取结果一致。
2.5 MVCC实践示例
sql
-- 初始数据:id=1, balance=1000
-- 事务A(事务ID=100)
BEGIN;
SELECT * FROM account WHERE id = 1; -- 读取balance=1000
-- 事务B(事务ID=101)
BEGIN;
UPDATE account SET balance = 2000 WHERE id = 1;
-- 此时生成新版本:balance=2000, DB_TRX_ID=101
-- 事务A继续
SELECT * FROM account WHERE id = 1; -- 仍然读取balance=1000(RR级别)
在RR级别下,事务A的ReadView在事务开始时创建,此时事务B尚未提交,所以事务B的修改对事务A不可见。
幻读是RR级别下的特殊问题,InnoDB通过两种机制解决:
-
快照读:通过MVCC读取历史版本,完全避免幻读
-
当前读:通过Next-Key Lock(行锁+间隙锁)锁定索引记录及其间隙,阻止其他事务插入
3. 幻读解决方案与Next-Key Lock
3.1 幻读的典型场景(RR级别)
假设存在表account,包含id=1, id=5, id=10三条记录:
重要说明:在RR级别下,快照读(普通SELECT)通过MVCC完全避免幻读,当前读(SELECT ... FOR UPDATE)通过Next-Key Lock避免幻读。
场景:RR级别下当前读的幻读(已被Next-Key Lock阻止)
sql
-- 事务A (RR级别)
BEGIN;
SELECT * FROM account WHERE id > 5 FOR UPDATE; -- 结果:id=10,加Next-Key Lock(5,10] + Gap Lock(10,+∞)
-- 事务B尝试插入
INSERT INTO account(id, balance) VALUES(8, 1000); -- 阻塞!等待事务A释放锁
-- 事务A继续
SELECT * FROM account WHERE id > 5 FOR UPDATE; -- 结果:id=10(无幻读,被锁保护)
COMMIT;
3.2 Next-Key Lock 详解
Next-Key Lock是InnoDB在RR级别下解决幻读的核心机制,它由行锁 + 间隙锁组成:
1. 三种锁类型
| 锁类型 | 说明 | 锁定范围 |
|---|---|---|
| Record Lock(行锁) | 锁定索引记录本身 | 仅锁定索引记录 |
| Gap Lock(间隙锁) | 锁定索引记录之间的间隙,不包含记录本身 | 防止其他事务插入 |
| Next-Key Lock(临键锁) | 行锁 + 间隙锁的组合 | 锁定记录及其前面的间隙 |
2. Next-Key Lock 的加锁规则
-
在RR隔离级别下,
SELECT ... FOR UPDATE、UPDATE、DELETE操作会加Next-Key Lock -
加锁的基本单位是Next-Key Lock(左开右闭区间)
-
查找过程中访问到的对象才会加锁
-
唯一索引上的等值查询,命中时Next-Key Lock退化为Record Lock
-
唯一索引上的范围查询,使用Next-Key Lock
-
非唯一索引上的等值查询,向右遍历且第一个不满足条件时,Next-Key Lock退化为Gap Lock

3. 加锁示例分析
假设表account有主键索引,包含id=1,5,10三条记录:
sql
-- 场景1:唯一索引等值查询(命中)
SELECT * FROM account WHERE id = 5 FOR UPDATE;
-- 加锁:Record Lock(id=5)
-- 场景2:唯一索引等值查询(未命中)
SELECT * FROM account WHERE id = 3 FOR UPDATE;
-- 加锁:Gap Lock(1,5) -- 锁定id=1和id=5之间的间隙
-- 场景3:唯一索引范围查询
SELECT * FROM account WHERE id > 5 AND id < 10 FOR UPDATE;
-- 加锁:Gap Lock(5,10) -- 锁定id=5和id=10之间的间隙,防止插入id=6,7,8,9等
-- 场景4:非唯一索引等值查询(假设balance有索引)
SELECT * FROM account WHERE balance = 1000 FOR UPDATE;
-- 加锁:对应索引上的Next-Key Lock + 主键上的Record Lock
4. 幻读被解决的原理
Next-Key Lock通过锁定索引记录及其前面的间隙,阻止其他事务在查询范围内插入新记录:
sql
-- 事务A
BEGIN;
SELECT * FROM account WHERE id > 5 FOR UPDATE; -- 加锁Next-Key Lock(5,10] + Gap Lock(10,+∞)
-- 事务B尝试插入
INSERT INTO account(id, balance) VALUES(8, 1000); -- 阻塞!等待事务A释放锁
-- 事务A继续执行
UPDATE account SET balance = 0 WHERE id > 5; -- 只影响id=10这一行
SELECT * FROM account WHERE id > 5 FOR UPDATE; -- 结果:id=10(无幻读)
COMMIT; -- 事务B的INSERT可以执行了
3.3 快照读与当前读的行为差异
重要规则:在RR级别下,快照读始终使用事务开始时创建的ReadView,不会因为执行当前读而改变。
sql
-- 事务A (RR级别)
BEGIN;
SELECT * FROM account WHERE id > 5; -- 快照读,结果:id=10
-- 事务B插入id=8并提交
INSERT INTO account(id, balance) VALUES(8, 1000);
COMMIT;
-- 事务A继续
SELECT * FROM account WHERE id > 5 FOR UPDATE; -- 当前读,结果:id=8, id=10
SELECT * FROM account WHERE id > 5; -- 快照读,结果:id=10(不变!ReadView未更新)
COMMIT;
结论:
-
RR级别下,快照读始终基于事务开始时的ReadView,不会看到新插入的数据
-
当前读(FOR UPDATE)会读取最新数据,但不会改变快照读的ReadView
-
只有再次执行当前读才能看到最新的已提交数据
4. 面试实战
4.1 高频问题汇总
MVCC核心
| 问题 | 关键答案 |
|---|---|
| MVCC解决什么问题? | 提高并发性能,实现读写不阻塞 |
| ReadView包含哪些字段? | m_ids, up_limit_id, low_limit_id, creator_trx_id |
| ReadView可见性判断? | trx_id < up_limit_id → 可见;在m_ids中 → 不可见 |
| RC和RR ReadView区别? | RC每次SELECT生成新的,RR只在事务开始时生成一次 |
| 版本链如何形成? | 通过DB_TRX_ID、DB_ROLL_PTR、Undo Log形成 |
幻读与锁机制
| 问题 | 关键答案 |
|---|---|
| RR级别如何解决幻读? | 快照读用MVCC解决,当前读用Next-Key Lock(行锁+间隙锁)解决 |
| 快照读 vs 当前读? | 普通SELECT vs FOR UPDATE/UPDATE/DELETE |
| Next-Key Lock是什么? | 行锁+间隙锁的组合,锁定索引记录及其前面的间隙 |
| 三种锁类型? | Record Lock(行锁)、Gap Lock(间隙锁)、Next-Key Lock(临键锁) |
| Next-Key Lock何时退化? | 唯一索引等值查询命中时退化为Record Lock |
4.2 追问清单与深入分析
面试官可能会深入追问以下问题,建议重点准备:
1. Next-Key Lock在什么情况下会退化?
-
唯一索引等值查询命中时退化为Record Lock
-
唯一索引等值查询未命中时退化为Gap Lock
-
非唯一索引等值查询向右遍历不满足条件时退化为Gap Lock
2. 什么是间隙锁的死锁风险?
sql
-- 场景:两个事务同时插入相同间隙
-- 事务A
BEGIN;
SELECT * FROM account WHERE id = 3 FOR UPDATE; -- 加Gap Lock(1,5)
-- 事务B
BEGIN;
SELECT * FROM account WHERE id = 4 FOR UPDATE; -- 加Gap Lock(1,5),阻塞等待A
-- 事务A
INSERT INTO account(id) VALUES(3); -- 成功
-- 事务B
INSERT INTO account(id) VALUES(4); -- 成功
-- 两个事务都能成功插入,不会死锁
真正的死锁场景:
sql
-- 事务A
BEGIN;
UPDATE account SET balance = 100 WHERE id = 1; -- 加Record Lock(1)
UPDATE account SET balance = 200 WHERE id = 5; -- 等待事务B释放id=5的锁
-- 事务B
BEGIN;
UPDATE account SET balance = 300 WHERE id = 5; -- 加Record Lock(5)
UPDATE account SET balance = 400 WHERE id = 1; -- 等待事务A释放id=1的锁
-- 死锁发生!
3. 快照读和当前读的区别?
| 读类型 | 实现方式 | 是否加锁 | 幻读风险 |
|---|---|---|---|
| 快照读 | MVCC读取历史版本 | 不加锁 | 无(在事务内) |
| 当前读 | 读取最新版本 + Next-Key Lock | 加锁 | 低(被锁阻止) |
4. 如何分析慢查询中的锁问题?
sql
-- 查看当前锁等待情况
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 查看当前锁信息
SELECT * FROM information_schema.INNODB_LOCKS;
-- 使用SHOW ENGINE INNODB STATUS查看详细信息
SHOW ENGINE INNODB STATUS;
5. 事务中的隐式锁是什么?
-
插入操作会生成隐式锁,防止其他事务读取未提交的数据
-
隐式锁在以下情况升级为显式锁:
-
其他事务对插入的行执行当前读
-
存在外键约束需要检查
-
INSERT操作在REPLACE或DELETE冲突时
-
总结
核心要点:
-
MVCC是InnoDB实现高并发的关键,通过版本链和ReadView实现读写不阻塞
-
RC vs RR的核心区别在于ReadView的生成时机(每次SELECT vs 事务开始时)
-
Next-Key Lock是解决幻读的关键机制,由行锁+间隙锁组成
-
快照读用MVCC,当前读用锁,两者结合解决并发问题
学习路径建议:
-
理解MVCC的版本链和ReadView机制
-
掌握RC和RR的ReadView区别
-
深入理解Next-Key Lock的加锁规则
-
通过实际SQL示例验证理论知识
记住,理论学习要与实践相结合。建议在本地搭建MySQL环境,亲自测试各种事务场景,这样理解会更深刻。