MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 存储引擎实现高并发读写的核心技术,核心目标是:在不加锁的情况下,让读事务(快照读)和写事务并发执行,避免读写冲突,同时保障事务隔离性(解决脏读、不可重复读等问题)。
简单说:MVCC 让 “读” 不用等 “写”,“写” 不用等 “读”,大幅提升数据库并发效率,是 InnoDB 区别于其他存储引擎(如 MyISAM)的关键特性之一。
一、MVCC 的核心目标:解决 “读写冲突”
在没有 MVCC 时,数据库并发控制依赖锁机制,但会出现严重问题:
- 读锁(S 锁)和写锁(X 锁)互斥:一个事务在读数据时,其他事务不能写;一个事务在写数据时,其他事务不能读 —— 导致 “读写阻塞”,并发效率极低。
MVCC 的优化思路:为数据维护多个版本,读事务读取 “历史版本”,写事务修改 “新版本”,两者互不干扰。具体来说:
- 写事务执行时,不会覆盖原始数据,而是生成一个新的数据版本;
- 读事务根据自身的 “可见性规则”,选择合适的历史版本读取,无需等待写事务提交;
- 旧版本数据不会立即删除,而是通过 “版本链” 管理,当不再被任何读事务引用时,由后台线程清理。
二、MVCC 的核心组成:隐藏列 + 版本链 + Read View
InnoDB 实现 MVCC 依赖三大核心组件,缺一不可:
1. 行数据的隐藏列(版本元数据)
InnoDB 表的每一行数据,除了用户定义的字段,还隐含 3 个核心隐藏列(用于维护版本信息):
| 隐藏列名 | 作用说明 |
|---|---|
DB_TRX_ID | 最近一次修改该数据的事务 ID(唯一标识一个事务,自增分配) |
DB_ROLL_PTR | 回滚指针,指向该数据的上一个版本(存储在 undo log 中),形成版本链 |
DB_ROW_ID | 隐含主键(若表无主键 / 唯一索引,InnoDB 自动生成),用于标识行唯一性 |
示例:假设表 user 有 id=1, name='张三', age=20,其实际存储结构(简化):
| id(用户列) | name(用户列) | age(用户列) | DB_TRX_ID(隐藏) | DB_ROLL_PTR(隐藏) | DB_ROW_ID(隐藏) |
|---|---|---|---|---|---|
| 1 | 张三 | 20 | 100 | 指向 undo log 中版本 1 | 10001 |
2. 版本链(基于 undo log 的历史版本链表)
每当事务修改数据时,InnoDB 不会直接覆盖原始数据,而是执行以下步骤:
- 将数据的 “原始版本” 复制到
undo log中(作为历史版本); - 修改当前行数据,更新
DB_TRX_ID为当前事务 ID,更新DB_ROLL_PTR指向 undo log 中的原始版本; - 多次修改后,undo log 中的历史版本通过
DB_ROLL_PTR串联,形成 “版本链”(最新版本在数据表中,旧版本在 undo log 中)。
版本链示例(3 次事务修改)
假设事务 100、200、300 依次修改 id=1 的数据:
- 事务 100(新增):
INSERT INTO user VALUES (1, '张三', 20)→DB_TRX_ID=100,DB_ROLL_PTR=NULL(无历史版本); - 事务 200(修改):
UPDATE user SET age=21 WHERE id=1→ 原始版本(age=20)存入 undo log,当前行DB_TRX_ID=200,DB_ROLL_PTR指向 undo log 中事务 100 的版本; - 事务 300(修改):
UPDATE user SET age=22 WHERE id=1→ 原始版本(age=21)存入 undo log,当前行DB_TRX_ID=300,DB_ROLL_PTR指向 undo log 中事务 200 的版本。
最终版本链结构(从新到旧):
当前行(age=22, TRX_ID=300) → undo log(age=21, TRX_ID=200) → undo log(age=20, TRX_ID=100)
3. Read View(读视图:判断版本可见性的规则)
版本链中存在多个历史版本,读事务需要知道 “哪个版本对自己可见”—— 这就是 Read View 的作用:事务启动时生成的 “快照”,包含当前活跃事务的 ID 范围,定义了版本可见性规则。
Read View 的核心属性
Read View 包含 4 个关键参数,用于判断版本是否可见:
| 属性名 | 作用说明 |
|---|---|
m_low_limit_id | 下一个将要分配的事务 ID(所有大于等于该 ID 的事务,对当前视图不可见) |
m_up_limit_id | 活跃事务中最小的事务 ID(所有小于该 ID 的事务,均已提交,对当前视图可见) |
m_ids | 生成 Read View 时,当前所有活跃(未提交)的事务 ID 集合 |
m_creator_trx_id | 生成 Read View 的事务自身的 ID(自身事务修改的版本,对自己可见) |
版本可见性判断规则(核心逻辑)
读事务在版本链中遍历(从最新版本到旧版本),对每个版本的 DB_TRX_ID(修改该版本的事务 ID)执行以下判断:
-
若
DB_TRX_ID < m_up_limit_id:该版本由已提交事务修改,可见,直接读取; -
若
DB_TRX_ID >= m_low_limit_id:该版本由未来事务修改,不可见,继续遍历旧版本; -
若
m_up_limit_id <= DB_TRX_ID < m_low_limit_id:- 若
DB_TRX_ID不在m_ids中(该事务已提交),可见; - 若
DB_TRX_ID在m_ids中(该事务仍活跃),不可见,继续遍历旧版本;
- 若
-
若遍历到版本链末尾仍无可见版本,返回空(或报错)。
关键结论
Read View 的生成时机,直接决定了事务的隔离级别(RC/RR)—— 这是 MVCC 与事务隔离性的核心关联。
三、MVCC 与事务隔离级别的关联(RC vs RR)
InnoDB 的 RC(读已提交) 和 RR(可重复读) 隔离级别,均基于 MVCC 实现,但核心差异在于:Read View 的生成时机不同。
1. RC 级别:每次查询都生成新的 Read View
- 规则:事务内每次执行
SELECT语句时,都会重新生成一个 Read View; - 效果:只能看到 “查询瞬间已提交” 的事务版本,避免脏读,但可能出现 “不可重复读”。
示例(RC 级别下的不可重复读)
| 时间顺序 | 事务 A(读事务) | 事务 B(写事务) |
|---|---|---|
| T1 | BEGIN; 开启事务 | - |
| T2 | SELECT age FROM user WHERE id=1 → 读 View1,age=20(可见事务 100 的版本) | - |
| T3 | - | BEGIN; 开启事务,UPDATE user SET age=21 WHERE id=1(DB_TRX_ID=200) |
| T4 | SELECT age FROM user WHERE id=1 → 读 View2,age=20(事务 B 未提交,200 在 m_ids 中,不可见) | - |
| T5 | - | COMMIT; 提交事务 B |
| T6 | SELECT age FROM user WHERE id=1 → 读 View3,age=21(事务 B 已提交,200 不在 m_ids 中,可见) | - |
- 结果:事务 A 两次查询同一数据,结果不一致(20→21),即 “不可重复读”—— 这是 RC 级别的特性。
2. RR 级别:事务启动时生成一次 Read View,全程复用
- 规则:事务启动(
BEGIN)后,第一次执行SELECT语句时生成 Read View,后续所有SELECT复用该 View; - 效果:事务内多次查询同一数据,始终看到 “第一次查询时的快照”,避免不可重复读和幻读(InnoDB 对 RR 的优化)。
示例(RR 级别下的可重复读)
| 时间顺序 | 事务 A(读事务) | 事务 B(写事务) |
|---|---|---|
| T1 | BEGIN; 开启事务 | - |
| T2 | SELECT age FROM user WHERE id=1 → 生成 View1,age=20(可见事务 100 的版本) | - |
| T3 | - | BEGIN; 开启事务,UPDATE user SET age=21 WHERE id=1(DB_TRX_ID=200) |
| T4 | SELECT age FROM user WHERE id=1 → 复用 View1,age=20(200 在 m_ids 中,不可见) | - |
| T5 | - | COMMIT; 提交事务 B |
| T6 | SELECT age FROM user WHERE id=1 → 复用 View1,age=20(200 仍在 View1 的 m_ids 中,不可见) | - |
| T7 | COMMIT; 提交事务 A,新事务查询 → age=21 | - |
- 结果:事务 A 全程复用同一个 Read View,即使事务 B 提交了修改,仍看不到新版本 —— 这就是 “可重复读”。
3. 隔离级别与 MVCC 总结
| 隔离级别 | Read View 生成时机 | MVCC 效果 | 解决的并发问题 |
|---|---|---|---|
| RC(读已提交) | 每次 SELECT 都生成新 View | 读已提交,允许不可重复读、幻读 | 禁止脏读 |
| RR(可重复读) | 事务内第一次 SELECT 生成,全程复用 | 可重复读,禁止幻读(InnoDB 优化) | 禁止脏读、不可重复读 |
| 读未提交 | 不生成 Read View,直接读最新版本 | 允许脏读、不可重复读、幻读 | 无 |
| 串行化 | 不用 MVCC,直接加表锁串行执行 | 完全隔离,无并发问题 | 所有并发问题 |
四、MVCC 的适用场景与限制
1. 适用场景(快照读)
MVCC 仅对 快照读(非锁定读) 生效,即:普通 SELECT 语句(不加锁):
SELECT * FROM user WHERE id=1; -- 快照读,走 MVCC,不加锁
2. 不适用场景(锁定读)
以下操作属于 锁定读,不走 MVCC,而是通过锁机制控制并发(避免修改冲突):
SELECT ... FOR SHARE(加 S 锁,共享读锁);SELECT ... FOR UPDATE(加 X 锁,排他写锁);UPDATE、DELETE、INSERT(写操作,默认加 X 锁)。
3. 版本清理机制(purge 线程)
版本链中的旧版本(undo log 中的历史数据)不会一直存在,否则会占用大量磁盘空间。InnoDB 有一个后台线程 purge 线程,负责:
- 定期扫描 undo log,判断哪些历史版本 “不再被任何 Read View 引用”(即没有读事务需要读取该版本);
- 将这些无用的旧版本删除,释放磁盘空间。
4. 限制
- 仅 InnoDB 支持 MVCC(MyISAM 不支持事务,也不支持 MVCC);
- 长事务会导致版本链过长(undo log 膨胀):因为长事务的 Read View 会一直引用旧版本,purge 线程无法清理,可能导致磁盘空间占用过高、崩溃恢复时间变长。
五、MVCC 与锁机制的区别(核心对比)
很多人会混淆 MVCC 和锁机制,两者的核心目标和适用场景完全不同:
| 对比维度 | MVCC(多版本并发控制) | 锁机制(行锁 / 表锁) |
|---|---|---|
| 核心目标 | 解决 “读写冲突”(读不阻塞写,写不阻塞读) | 解决 “写写冲突”(防止多个事务同时修改同一数据) |
| 适用场景 | 快照读(普通 SELECT) | 锁定读 / 写操作(FOR UPDATE、UPDATE、DELETE 等) |
| 并发效率 | 高(无锁,非阻塞) | 中 / 低(加锁,可能阻塞) |
| 数据一致性 | 基于快照,保证隔离级别(RC/RR) | 基于锁竞争,保证修改原子性 |
六、总结
MVCC 是 InnoDB 实现高并发读写的核心技术,其本质是:通过 “隐藏列维护版本元数据”+“undo log 构建版本链”+“Read View 控制可见性”,让读事务读取历史版本,写事务生成新版本,从而实现读写不阻塞。
关键要点:
- MVCC 的核心是 “多版本”,通过版本链隔离读写操作;
- Read View 的生成时机决定隔离级别:RC 每次查询生成,RR 事务内复用;
- MVCC 仅对普通 SELECT(快照读)生效,写操作仍依赖锁机制;
- 避免长事务,防止版本链过长导致的性能问题。