MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB存储引擎解决读写冲突、提升并发性能的核心技术——它通过为数据行维护多个版本(快照),让读操作(SELECT)无需加锁即可读取数据,实现读不阻塞写、写不阻塞读的高并发效果。

MVCC是InnoDB专为读已提交(RC) 和可重复读(RR) 隔离级别设计的并发控制机制,核心特征如下:
SELECT) 生效,当前读(SELECT ```FOR UPDATE/LOCK IN SHARE MODE、INSERT/UPDATE/DELETE)仍需加锁;InnoDB的MVCC依赖行记录隐藏字段、Undo Log(回滚日志)、Read View(读视图) 三大核心组件,三者协同完成多版本数据的管理和读取。
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)
Undo Log是InnoDB在修改数据时,记录的“数据修改前的快照”,是实现版本链的核心载体。
Insert Undo Log:仅记录INSERT操作的日志,事务提交后可直接删除(因INSERT的记录仅当前事务可见,无版本回溯需求);Update Undo Log:记录UPDATE/DELETE操作的日志,事务提交后需保留(供其他事务的MVCC读取历史版本),直到没有事务需要访问该版本时,由Purge线程清理。每次更新行数据时,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)
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); |
事务读取行数据时,通过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回溯版本链,直到找到第一个可见的版本(或无可见版本)。以MySQL默认的可重复读(RR) 隔离级别为例,拆解MVCC在INSERT/UPDATE/DELETE/SELECT中的执行流程。
INSERT INTO user(id, name) VALUES (1, '张三');DB_TRX_ID=100,DB_ROLL_PTR=NULL(无历史版本);Insert Undo Log(仅用于事务回滚,提交后删除);UPDATE user SET name='李四' WHERE id=1;DB_TRX_ID=100)写入Update Undo Log;DB_TRX_ID=101,DB_ROLL_PTR指向刚生成的Update Undo Log;Update Undo Log保留,供其他事务读取历史版本。DELETE被InnoDB视为“特殊的UPDATE”,执行流程如下:
DELETE FROM user WHERE id=1;Update Undo Log;DB_TRX_ID=102,并标记“删除标识”(物理删除由Purge线程异步完成);假设当前有活跃事务ID:101、102,事务T4(ID=103)执行SELECT * FROM user WHERE id=1(RR隔离级别):
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 → 不可见;DB_TRX_ID=100):100 < min_trx_id(101) → 可见;name='张三');MVCC仅在读已提交(RC) 和可重复读(RR) 隔离级别生效,核心区别是Read View的创建时机:
| 隔离级别 | Read View创建时机 | 读取结果特点 |
|---|---|---|
| 读已提交(RC) | 每次执行快照读(SELECT)时,重新创建Read View | 同一事务内多次SELECT可能读取到不同版本(不可重复读),仅能看到已提交的最新版本; |
| 可重复读(RR) | 事务内第一次执行快照读时创建Read View,后续复用 | 同一事务内多次SELECT读取结果一致(可重复读),仅能看到事务启动时已提交的版本; |
| 读未提交(RU) | 不使用MVCC,直接读取最新数据(无版本控制) | 能看到未提交事务的数据,存在脏读; |
| 串行化(SERIALIZABLE) | 禁用MVCC,所有读操作加表锁 | 完全串行执行,无并发冲突,但性能极低; |
SELECT ```FOR UPDATE)仍需加锁,无法避免写冲突。DB_TRX_ID/DB_ROLL_PTR)、Undo Log版本链、Read View可见性规则实现;