一、概述
1.1 MVCC的定义与价值
MVCC(Multi-Version Concurrency Control)是一种非锁定式并发控制技术,其核心目标是解决读写操作的相互阻塞问题。传统锁机制中,读操作加共享锁、写操作加排他锁,导致读写互斥;而MVCC通过维护数据的多个历史版本,让快照读(非锁定读)无需等待写操作释放锁,写操作也不会阻塞读操作,仅写写操作需通过锁机制控制冲突,显著提升InnoDB的并发处理能力。
1.2 关键区分:当前读与快照读
MVCC的应用依赖于InnoDB对读操作的两种分类,二者决定了是否触发多版本控制逻辑:
| 读操作类型 | 核心特征 | 典型 SQL 场景 | 是否依赖 MVCC |
|---|---|---|---|
| 当前读 | 读取数据最新版本,加锁阻塞其他事务修改 | select ... lock in share mode(共享锁)、select ... for update(排他锁)、update/delete/insert |
否(基于悲观锁实现) |
| 快照读 | 读取数据历史版本,无锁非阻塞,结果可能非最新 | 普通select(如select * from user) |
是(MVCC 的核心应用场景) |
特殊场景:串行隔离级别(SERIALIZABLE)下,快照读会退化为当前读,强制事务顺序执行,避免并发冲突。
1.3 MVCC解决的并发问题
数据库并发操作存在三类冲突场景,MVCC的作用范围如下:
- 读-读冲突:无影响,无需控制;
- 读-写冲突:MVCC的核心解决场景,通过版本隔离避免脏读、不可重复读,结合间隙锁可解决幻读;
- 写-写冲突:MVCC无法处理,需依赖行锁(悲观锁)或版本号(乐观锁)防止更新丢失。
具体而言,MVCC通过“事务快照”机制确保读操作仅能看到事务开始前已提交的版本,从而规避三类异常:
- 脏读:读取未提交事务的修改数据;
- 不可重复读:同一事务内多次读取同一数据,结果不一致;
- 幻读:同一事务内多次查询,结果集行数变化(InnoDB在RR级别下通过MVCC+间隙锁解决)。
二、实现原理
InnoDB的MVCC并非直接存储数据多版本,而是通过隐式字段、undo日志、Read View三者协同,实现版本回溯与可见性判断,其底层逻辑可拆解为“版本记录-版本回溯-版本筛选”三个环节。
2.1 隐式字段
InnoDB为每一行数据自动添加3个隐式字段(用户不可直接查询),用于记录版本关联信息:
- DB_TRX_ID(6字节):记录创建或最后一次修改该行数据的事务ID(事务ID全局递增,确保唯一性);
- DB_ROW_ID(6字节):隐含自增主键,若表未定义主键,InnoDB会以此作为聚簇索引主键,保证数据行唯一性。
- DB_ROLL_PTR(7字节):回滚指针,指向该行数据的上一个历史版本(存储于undo日志中),形成“版本链”;

此外,还有一个隐藏的删除标记(deleted_bit):执行delete或update时,并非物理删除数据,而是将该标记设为1,后续由purge线程清理无效版本。
2.2 undo日志
undo日志(回滚日志)是存储数据历史版本的核心,分为两类,其中update undo log是MVCC实现的关键:
| undo 日志类型 | 产生场景 | 核心作用 | 清理时机 |
|---|---|---|---|
| insert undo log | 执行insert操作时 |
仅用于事务回滚(若事务回滚,需删除插入的新行) | 事务提交后立即删除 |
| update undo log | 执行update/delete操作时 |
1. 事务回滚;2. 快照读时回溯历史版本 | 无事务依赖时(由 purge 线程清理) |
2.2.1 版本链的生成过程
以person表中一条数据(id=1,name=Jerry,age=24)的两次修改为例,展示版本链的形成逻辑:
- 初始插入:事务0(系统初始事务)插入数据,DB_TRX_ID=NULL,DB_ROLL_PTR=NULL(无历史版本);

- 事务1修改name为“Tom”:
- 加排他锁阻止其他事务修改;
- 将当前版本(name=Jerry)拷贝到undo日志,作为历史版本;
- 更新原数据name为“Tom”,DB_TRX_ID=1,DB_ROLL_PTR指向undo日志中的历史版本;
- 事务提交,释放锁;

- 事务2修改age为30:
- 重复上述步骤,将当前版本(name=Tom,age=30)存入undo日志;
- 更新原数据age为30,DB_TRX_ID=2,DB_ROLL_PTR指向新的undo日志版本;
- 事务提交,释放锁。
最终形成的版本链(由DB_ROLL_PTR串联)如下:

原数据行通过DB_ROLL_PTR关联undo日志中的历史版本,形成链式结构
2.2.2 purge线程的清理机制
为避免undo日志无限膨胀,InnoDB的purge线程会定期清理满足以下条件的update undo log:
- 数据行的删除标记(deleted_bit)为1;
- 该历史版本对所有活跃事务均不可见(通过purge线程维护的Read View判断)。
例如,若所有事务仅能看到“name=Tom,age=30”或“name=Tom,age=24”的版本,那么“name=Jerry,age=24”的undo日志版本会被清理。
2.3 Read View
当事务执行快照读时,InnoDB会生成一个Read View(读视图),用于判断当前事务能看到哪个版本的数据。Read View本质是快照读生成时刻的“系统事务状态快照”,核心作用是筛选undo日志中的历史版本,找到符合可见性规则的最新版本。
2.3.1 核心属性
简化后的Read View包含三个关键参数:
- trx_list:Read View生成时,系统中所有活跃事务(未提交)的ID列表;
- up_limit_id:trx_list中的最小事务ID(低水位,小于该ID的事务均已提交);
- low_limit_id:Read View生成时,系统尚未分配的下一个事务ID(高水位,大于该ID的事务均未开启)。
2.3.2 版本可见性判断规则
对于undo日志中的某一历史版本(DB_TRX_ID为修改该版本的事务ID),需满足以下条件之一才对当前事务可见:
- DB_TRX_ID < up_limit_id:修改该版本的事务在Read View生成前已提交,可见;
- DB_TRX_ID == 当前事务ID:该版本由当前事务修改,可见;
- DB_TRX_ID > low_limit_id:修改该版本的事务在Read View生成后才开启,不可见;
- DB_TRX_ID 不在 trx_list中:修改该版本的事务在Read View生成前已提交,可见;
- DB_TRX_ID 在trx_list中:修改该版本的事务仍活跃(未提交),不可见。
若当前版本不可见,则通过DB_ROLL_PTR回溯到上一版本,重复判断,直到找到可见版本或返回空。
三、工作流程
结合上述组件,以“事务3快照读user表id=1的数据”为例,还原MVCC的完整执行逻辑:
3.1 场景假设
- 系统当前事务状态:事务1(活跃未提交)、事务2(已提交)、事务3(当前执行快照读)、事务4(活跃未提交);
- Read View生成时:trx_list=[1,4](活跃事务ID),up_limit_id=1(低水位),low_limit_id=5(高水位,下一个事务ID);

- user表当前数据:id=1,name=李四,age=25,DB_TRX_ID=2(事务2修改),DB_ROLL_PTR指向事务1的undo日志版本(name=李四,age=20)。

3.2 执行步骤
- 生成Read View:事务3执行
select * from user where id=1(快照读),InnoDB生成包含trx_list=[1,4]、up_limit_id=1、low_limit_id=5的Read View; - 读取当前版本:获取user表id=1的当前数据(name=李四,age=25),其DB_TRX_ID=2;
- 判断可见性:
- 2 > up_limit_id(1),不满足条件1;
- 2 != 当前事务 ID(3),不满足条件2;
- 2 < low_limit_id(5),不满足条件3;
- 2 不在 trx_list([1,4])中,满足条件4→版本可见;
- 返回结果:事务3读取到name=李四,age=25的数据。
若事务2未提交(DB_TRX_ID=2在trx_list中),则当前版本不可见,事务3会通过DB_ROLL_PTR回溯到事务1的版本(name=李四,age=20,DB_TRX_ID=1),因1在trx_list中(事务1活跃),仍不可见;继续回溯到事务0的初始版本(name=张三,age=20,DB_TRX_ID=0<up_limit_id=1),满足条件1,最终返回该版本。

四、差异:RC与RR级别下的MVCC行为
InnoDB的RC(读已提交)和RR(可重复读)隔离级别均依赖MVCC实现,但快照读行为存在显著差异,核心原因是Read View的生成时机不同,这直接影响事务对“其他事务提交修改”的可见性。
4.1 RR级别:如何实现“可重复读”
RR级别通过“事务内首次快照读生成Read View,后续快照读复用该Read View”的机制,确保同一事务内多次读取结果一致,解决不可重复读问题。
案例演示(RR级别)
| 时间线 | 事务 A(RR 级别) | 事务 B(RR 级别) |
|---|---|---|
| T1 | 开启事务,执行select age from user where id=1 → 结果 20(生成 Read View1:trx_list=[], up_limit_id=1, low_limit_id=2) |
- |
| T2 | - | 开启事务,执行update user set age=25 where id=1,提交事务(DB_TRX_ID=2) |
| T3 | 再次执行select age from user where id=1 → 结果仍为 20(复用 Read View1,事务 B 的修改不可见) |
- |
原因:事务A在T1生成的Read View1中,low_limit_id=2,事务B的DB_TRX_ID=2等于low_limit_id,根据规则3,修改不可见,因此两次读取结果一致。
4.2 RC级别:为何“不可重复读”
RC级别下,事务内每次快照读都会重新生成Read View,导致同一事务内多次读取可能看到其他事务提交的修改,出现不可重复读。
案例演示(RC级别)
| 时间线 | 事务 A(RC 级别) | 事务 B(RC 级别) |
|---|---|---|
| T1 | 开启事务,执行select age from user where id=1 → 结果 20(生成 Read View1:trx_list=[], up_limit_id=1, low_limit_id=2) |
- |
| T2 | - | 开启事务,执行update user set age=25 where id=1,提交事务(DB_TRX_ID=2) |
| T3 | 再次执行select age from user where id=1 → 生成 Read View2(trx_list=[], up_limit_id=3, low_limit_id=3),结果 25(事务 B 的修改可见) |
- |
原因:事务A在T3重新生成的Read View2中,up_limit_id=3,事务B的DB_TRX_ID=2 < up_limit_id,根据规则1,修改可见,因此两次读取结果不同。
4.3 RC与RR级别MVCC差异总结
| 隔离级别 | Read View 生成时机 | 快照读结果一致性 | 其他事务提交修改的可见性 |
|---|---|---|---|
| RR(可重复读) | 事务内首次快照读生成,后续复用 | 同一事务内多次读取结果一致 | 不可见(除非事务结束后重新开启) |
| RC(读已提交) | 事务内每次快照读重新生成 | 同一事务内多次读取结果可能不一致 | 可见(下次快照读即可看到) |
五、MVCC的价值与局限
5.1 核心价值
- 并发性能提升:实现“读不阻塞写、写不阻塞读”,仅写写操作加锁,大幅降低锁竞争;
- 事务隔离保障:在RR级别下通过固定Read View解决不可重复读,结合间隙锁解决幻读,满足多数业务的一致性需求;
- 锁操作简化:快照读无需加锁,减少死锁风险,降低应用层锁管理复杂度。
5.2 局限性
- 无法处理写写冲突:需依赖行锁(如
select ... for update)或乐观锁(如版本号字段)防止更新丢失; - undo日志开销:历史版本存储占用磁盘空间,若事务长期未提交,可能导致undo日志膨胀;
- 版本判断性能损耗:快照读时需遍历版本链并通过Read View判断可见性,版本链过长会影响查询效率。
六、总结
MVCC是InnoDB实现高并发与数据一致性的核心机制,其本质是通过“隐式字段记录版本关联、undo日志存储历史版本、Read View筛选可见版本”