第一部分:架构师眼中的并发控制逻辑
面试官最喜欢问:"为什么 MySQL 在读写并发时,不需要像 Java 的 ReentrantReadWriteLock 那样阻塞写操作?"
1. 什么是 MVCC?
MVCC(Multi-Version Concurrency Control)即多版本并发控制。它是通过在每个数据行上维护多个版本的数据来实现的并发控制机制。
当一个事务修改数据时,MVCC 会为该事务创建数据的快照,而不是直接修改原始行,从而避免了读写冲突。
2. 快照读与当前读
作为架构师,你必须区分这两者:
-
一致性非锁定读(快照读) :普通的
SELECT语句(不加锁)。基于事务开始时的状态创建快照,不读取其他未提交事务的修改。 -
锁定读(当前读) :执行
SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE等。它读取的是数据的最新版本,并对记录加锁(S 锁或 X 锁)。
第二部分:MVCC 的三大底层基石
MVCC 的实现不是空中楼阁,它依赖于三个核心组件:隐藏字段、Read View、undo log。
1. 隐藏字段:数据行的"秘密档案"
InnoDB 会为每行数据自动添加三个隐藏字段:
-
DB_TRX_ID(6字节):记录最后一次修改该行的事务 ID。 -
DB_ROLL_PTR(7字节) :回滚指针,指向undo log中的历史版本。 -
DB_ROW_ID(6字节):如果没有主键,则以此生成聚簇索引。
2. Read View:事务的"时间切片"
Read View 保存了"当前对本事务不可见的其他活跃事务 ID 列表"。它主要包含:
-
m_ids:创建 Read View 时活跃且未提交的事务 ID 列表。 -
m_up_limit_id:活跃事务列表中最小的事务 ID。 -
m_low_limit_id:下一个将被分配的事务 ID(最大 ID+1)。 -
m_creator_trx_id:创建该 Read View 的事务 ID。
3. Undo Log:数据的"后悔药"
undo log 分为两种:
-
insert undo log:事务提交后可立即删除。 -
update undo log:用于实现 MVCC。不同事务对同一行的修改会通过DB_ROLL_PTR形成一个版本链。
第三部分:数据可见性算法------MVCC 的灵魂
面试官:"InnoDB 怎么知道哪个版本的数据是我能看的?"
核心判定逻辑:
-
DB_TRX_ID < m_up_limit_id:修改该行的事务在快照创建前已提交,可见。 -
DB_TRX_ID >= m_low_limit_id:修改该行的事务在快照创建后才开启,不可见。 -
在
m_up_limit_id和m_low_limit_id之间:-
检查
DB_TRX_ID是否在活跃列表m_ids中。 -
在列表中 :说明创建快照时该事务未提交,不可见。
-
不在列表中 :说明创建快照前该事务已提交,可见。
-
-
不可见怎么办? 沿着
DB_ROLL_PTR寻找undo log中的上一版本,重复上述判断。
第四部分:RC 与 RR 隔离级别的差异实战
MVCC 在不同隔离级别下生成 Read View 的时机截然不同,这决定了结果的差异。
| 特性 | Read Committed (RC) | Repeatable Read (RR) |
|---|---|---|
| 生成时机 | 每次 SELECT 前都会生成新的 Read View | 只在事务第一次 SELECT 前生成一个 Read View |
| 结果影响 | 导致不可重复读 | 实现了可重复读 |
💡 Java 实战示例:如何在代码中感受这种差异
你应该通过代码压测来验证数据库的隔离行为。
Java
// 使用 Spring 的事务管理来模拟并发场景
@Service
public class TradeService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 演示在 RR 隔离级别下的可重复读
* 假设数据库默认隔离级别为 RR
*/
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void demoRR() {
// 1. 第一次快照读:生成 Read View
String name1 = jdbcTemplate.queryForObject("SELECT name FROM users WHERE id = 1", String.class);
System.out.println("第一次读取:" + name1);
// 此时,外部有事务修改了数据并提交
// 2. 第二次快照读:沿用第一次的 Read View
// 即便外部数据变了,MVCC 通过版本链找到旧版本,保证两次读取一致
String name2 = jdbcTemplate.queryForObject("SELECT name FROM users WHERE id = 1", String.class);
System.out.println("第二次读取:" + name2);
}
}
第五部分:MVCC 真的彻底解决了幻读吗?
面试官的"夺命连环炮":"MVCC 怎么解决幻读?"
严谨回答:
-
快照读下 :RR 隔离级别通过 MVCC 解决幻读。因为
Read View是一次性生成的,后续其他事务插入的数据版本对当前事务不可见。 -
当前读下 :MVCC 无法生效,因为当前读必须拿最新数据。InnoDB 使用 Next-key Lock(行锁+间隙锁)来锁定读取范围,禁止其他事务在此区间插入,从而防止幻读。
第六部分:面试复盘脑图
为了帮你快速记忆,我整理了这张 MVCC 核心知识树:
Code snippet
mindmap
root((InnoDB MVCC 实现))
核心定义
多版本并发控制: 读不加锁, 读写不冲突
快照读: 普通 SELECT
当前读: SELECT FOR UPDATE, DML 语句
三大支柱
隐藏字段: DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID
Read View: 活跃事务快照
Undo Log: insert/update 版本链
可见性判定算法
判定逻辑: 基于事务ID与活跃列表对比
回溯机制: 沿 DB_ROLL_PTR 查找历史版本
级别差异
RC: 每次查询生成新 Read View (不可重复读)
RR: 仅第一次查询生成 Read View (可重复读)
幻读攻防
快照读幻读: 依靠 MVCC 屏蔽新版本
当前读幻读: 依靠 Next-key Lock 锁定间隙
结语:从原理到信仰
理解 MVCC 的底层逻辑,能让你在处理复杂的并发业务时拥有"上帝视角"。
InnoDB MVCC 不是负担,而是高并发系统的底气。 如果你能理顺隐藏字段的"档案记录"、Read View 的"时间定格"以及 Undo Log 的"时空回溯",那么你在面试官眼中,就是一个深谙底层原理的资深大牛。
这篇文章只是冰山一角。如果你对"MVCC 版本的回收(Purge 操作)"或者"间隙锁的具体加锁区间"感兴趣,请在评论区留言。