MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB存储引擎解决读写冲突、提升并发性能 的核心技术------它通过为数据行维护多个版本(快照),让读操作(SELECT)无需加锁即可读取数据,实现读不阻塞写、写不阻塞读的高并发效果。
一、MVCC核心概念
1.1 什么是MVCC
MVCC是InnoDB专为读已提交(RC) 和可重复读(RR) 隔离级别设计的并发控制机制,核心特征如下:
- 核心思想:为每一行数据维护多个版本(快照),不同事务读取时,根据规则选择对应版本的数据,而非直接读取最新数据;
- 适用场景:仅针对快照读(普通
SELECT) 生效,当前读(SELECT ```FOR UPDATE/LOCK IN SHARE MODE、INSERT/UPDATE/DELETE)仍需加锁; - 目标:解决"读写冲突",避免传统锁机制中"读阻塞写、写阻塞读"的问题,提升并发性能。
1.2 MVCC的核心目标
- 读操作无锁化:快照读无需加行锁/表锁,不阻塞写操作;
- 数据一致性:保证不同事务在各自隔离级别下,读取到符合规则的一致数据;
- 历史版本可回溯:通过版本链,支持事务读取"过去某个时间点"的数据(如RR隔离级别的可重复读)。
二、MVCC实现的三大基础组件
InnoDB的MVCC依赖行记录隐藏字段 、Undo Log(回滚日志) 、Read View(读视图) 三大核心组件,三者协同完成多版本数据的管理和读取。
2.1 行记录的隐藏字段
InnoDB为每一行数据(除自定义字段外)自动添加3个隐藏字段,是MVCC的基础:
| 隐藏字段 | 字段类型 | 核心作用 |
|---|---|---|
DB_TRX_ID |
6字节 | 记录最后一次插入/更新该行数据的事务ID(删除视为特殊的更新,标记删除); |
DB_ROLL_PTR |
7字节 | 回滚指针,指向该行数据的Undo Log版本链(通过该指针可回溯历史版本); |
DB_ROW_ID |
6字节 | 聚簇索引无主键/唯一键时,InnoDB自动生成的行ID(仅用于标识行,非MVCC核心); |
示例 :
假设有表user(id INT PRIMARY KEY, name VARCHAR(20)),插入一行(1, '张三'),该行的实际存储结构为:
| id | name | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
|---|---|---|---|---|
| 1 | 张三 | 100 | 指向Undo Log | NULL |
(注:DB_TRX_ID=100表示插入该记录的事务ID为100)
2.2 Undo Log(回滚日志)
Undo Log是InnoDB在修改数据时,记录的"数据修改前的快照",是实现版本链的核心载体。
2.2.1 Undo Log的类型
Insert Undo Log:仅记录INSERT操作的日志,事务提交后可直接删除(因INSERT的记录仅当前事务可见,无版本回溯需求);Update Undo Log:记录UPDATE/DELETE操作的日志,事务提交后需保留(供其他事务的MVCC读取历史版本),直到没有事务需要访问该版本时,由Purge线程清理。
2.2.2 版本链(Version Chain)
每次更新行数据时,InnoDB会按以下步骤维护版本链:
- 将更新前的数据写入
Update Undo Log; - 更新行记录的
DB_TRX_ID为当前事务ID; - 更新行记录的
DB_ROLL_PTR,指向刚生成的Update Undo Log; - 多次更新后,
DB_ROLL_PTR会串联所有历史版本,形成版本链(链头是最新版本,链尾是最早版本)。
版本链示例:
当前行版本(DB_TRX_ID=102) → DB_ROLL_PTR → 版本1(DB_TRX_ID=101) → DB_ROLL_PTR → 版本0(DB_TRX_ID=100)
2.3 Read View(读视图)
Read View是事务执行快照读时,生成的一个"可见性判断规则",核心作用是:判断当前事务能看到哪些版本的数据。
2.3.1 Read View的核心字段
Read View包含4个关键字段,用于版本可见性判断:
| 字段名 | 含义 |
|---|---|
m_ids |
生成Read View时,当前活跃的事务ID集合(未提交的事务ID); |
min_trx_id |
m_ids中的最小事务ID(活跃事务的最小ID); |
max_trx_id |
系统下一个要分配的事务ID(大于当前所有已分配的事务ID); |
creator_trx_id |
创建该Read View的事务ID(当前执行快照读的事务ID); |
2.3.2 版本可见性判断规则
事务读取行数据时,通过Read View判断该行的某个版本是否可见:
假设待判断版本的DB_TRX_ID = trx_id,规则如下:
- 若
trx_id < min_trx_id:该版本由"已提交的事务"生成,可见; - 若
trx_id >= max_trx_id:该版本由"未来的事务"生成(当前事务未开始时,该事务还未创建),不可见; - 若
min_trx_id ≤ trx_id < max_trx_id:- 若
trx_id ∈ m_ids:该版本由"当前活跃的未提交事务"生成,不可见; - 若
trx_id ∉ m_ids:该版本由"已提交的事务"生成,可见;
- 若
- 若版本不可见,则通过
DB_ROLL_PTR回溯版本链,直到找到第一个可见的版本(或无可见版本)。
三、MVCC核心执行逻辑(以RR隔离级别为例)
以MySQL默认的可重复读(RR) 隔离级别为例,拆解MVCC在INSERT/UPDATE/DELETE/SELECT中的执行流程。
3.1 数据插入(INSERT)
- 事务T1(ID=100)执行
INSERT INTO user(id, name) VALUES (1, '张三'); - InnoDB为该行数据写入
DB_TRX_ID=100,DB_ROLL_PTR=NULL(无历史版本); - 生成
Insert Undo Log(仅用于事务回滚,提交后删除); - 事务提交后,该行数据的版本仅对已提交的事务可见。
3.2 数据更新(UPDATE)
- 事务T2(ID=101)执行
UPDATE user SET name='李四' WHERE id=1; - InnoDB先将该行当前版本(
DB_TRX_ID=100)写入Update Undo Log; - 更新行记录的
DB_TRX_ID=101,DB_ROLL_PTR指向刚生成的Update Undo Log; - 此时版本链:当前版本(101)→ 历史版本(100);
- 事务提交后,
Update Undo Log保留,供其他事务读取历史版本。
3.3 数据删除(DELETE)
DELETE被InnoDB视为"特殊的UPDATE",执行流程如下:
- 事务T3(ID=102)执行
DELETE FROM user WHERE id=1; - InnoDB将该行当前版本写入
Update Undo Log; - 更新行记录的
DB_TRX_ID=102,并标记"删除标识"(物理删除由Purge线程异步完成); - 版本链新增一条:当前版本(102,标记删除)→ 版本1(101)→ 版本0(100)。
3.4 数据读取(SELECT,快照读)
假设当前有活跃事务ID:101、102,事务T4(ID=103)执行SELECT * FROM user WHERE id=1(RR隔离级别):
- T4第一次执行SELECT时,生成Read View:
m_ids = {101, 102},min_trx_id=101,max_trx_id=104,creator_trx_id=103;
- 读取行数据的当前版本(
DB_TRX_ID=102),判断可见性:102 ≥ min_trx_id(101)且102 < max_trx_id(104),且102 ∈ m_ids→ 不可见;
- 通过
DB_ROLL_PTR回溯版本链,读取上一个版本(DB_TRX_ID=101):101 ∈ m_ids→ 不可见;
- 继续回溯到版本0(
DB_TRX_ID=100):100 < min_trx_id(101)→ 可见;
- 返回该版本的数据(
name='张三'); - RR隔离级别下,T4后续的SELECT会复用该Read View,因此多次读取结果一致(可重复读)。
四、不同隔离级别下的MVCC行为
MVCC仅在读已提交(RC) 和可重复读(RR) 隔离级别生效,核心区别是Read View的创建时机:
| 隔离级别 | Read View创建时机 | 读取结果特点 |
|---|---|---|
| 读已提交(RC) | 每次执行快照读(SELECT)时,重新创建Read View | 同一事务内多次SELECT可能读取到不同版本(不可重复读),仅能看到已提交的最新版本; |
| 可重复读(RR) | 事务内第一次执行快照读时创建Read View,后续复用 | 同一事务内多次SELECT读取结果一致(可重复读),仅能看到事务启动时已提交的版本; |
| 读未提交(RU) | 不使用MVCC,直接读取最新数据(无版本控制) | 能看到未提交事务的数据,存在脏读; |
| 串行化(SERIALIZABLE) | 禁用MVCC,所有读操作加表锁 | 完全串行执行,无并发冲突,但性能极低; |
五、MVCC的优势与局限
5.1 优势
- 高并发:快照读无锁,读写互不阻塞,大幅提升并发性能;
- 数据一致性:在RC/RR隔离级别下,保证读取数据的一致性,避免脏读(RC/RR)、不可重复读(RR);
- 无锁读:无需加行锁/表锁,减少锁竞争和死锁概率。
5.2 局限
- 版本链开销:大量更新操作会导致版本链过长,增加读取时的回溯成本;
- Undo Log清理:Purge线程需异步清理过期的Undo Log,若清理不及时会占用磁盘空间;
- 仅适用于快照读 :当前读(如
SELECT ```FOR UPDATE)仍需加锁,无法避免写冲突。
总结
- 核心依赖 :MVCC基于行记录隐藏字段(
DB_TRX_ID/DB_ROLL_PTR)、Undo Log版本链、Read View可见性规则实现; - 核心逻辑:更新数据时生成版本链,读取数据时通过Read View判断版本可见性,回溯版本链找到符合规则的版本;
- 隔离级别差异:RC每次SELECT创建Read View(不可重复读),RR仅第一次创建(可重复读);
- 核心价值:实现"读不阻塞写、写不阻塞读",是InnoDB高并发的核心保障。