← 返回文章

MVCC

发布于 2026/10/18

1. 完整回答

MVCC,全称 Multi-Version Concurrency Control,也就是多版本并发控制,主要用于解决数据库事务并发读写时的数据一致性和性能问题。它的核心思想是保留数据的历史版本,让不同事务根据各自的可见性规则读取合适的数据版本,从而减少读写之间的锁冲突。

以 MySQL InnoDB 为例,MVCC 主要依赖三个部分:隐藏字段、Undo Log 和 Read View。

当事务修改数据时,InnoDB 会记录 Undo Log,用于恢复历史版本,并通过回滚指针关联版本。普通 SELECT 执行快照读时,会根据 Read View 判断当前版本是否可见。如果不可见,就沿着 Undo Log 查找符合条件的历史版本。

比如一条记录原来的值是 100,事务 A 已经建立快照,事务 B 将值修改为 200 并提交。在可重复读隔离级别下,事务 A 再次进行普通查询,仍然可以看到 100,而后续建立新快照的事务可以看到 200。

MVCC 的主要优势是普通快照读通常不阻塞写入,写入也通常不阻塞快照读,能够提高并发性能。同时,通过不同的 Read View 生成策略,支持读已提交和可重复读两种隔离级别。

不过,MVCC 主要用于一致性读。对于 UPDATE、DELETE、SELECT ... FOR UPDATE 等操作,InnoDB 仍然需要配合锁机制处理并发冲突。

因此,MVCC 广泛应用于订单查询、业务数据读取、报表统计等读写并发场景。

2. 核心原理与关键概念

2.1 核心原理

InnoDB 的 MVCC 可以概括为:

数据修改 → 构建历史版本 → 创建 Read View → 判断版本可见性 → 返回符合条件的数据。

  1. 数据修改:事务更新记录时,InnoDB 修改当前记录,并通过 Undo Log 保留恢复旧版本所需的信息。

  2. 构建版本链:当前记录通过回滚指针关联历史版本,形成可供追溯的版本链。

  3. 创建 Read View:执行一致性读时,按照事务隔离级别创建或复用 Read View,记录用于判断事务可见性的信息。

  4. 判断可见性:比较记录的事务 ID 和 Read View。如果当前版本可见,就直接返回;否则继续查找更早的版本。

  5. 返回数据:找到第一个可见版本后返回。对于当前快照不可见的新增记录,查询结果中不会出现该记录。

2.2 关键概念

概念 作用
DB_TRX_ID 记录最近一次插入或更新该行的事务 ID,用于判断版本可见性
DB_ROLL_PTR 回滚指针,指向 Undo Log 中对应的历史信息
Undo Log 保存回滚及恢复旧版本需要的信息,支持事务回滚和一致性读
Read View 读视图,根据创建时的事务状态判断哪些数据版本可见
版本链 通过当前记录和 Undo Log 关联起来的历史版本关系

其中,DB_TRX_ID 和 DB_ROLL_PTR 是理解 MVCC 最关键的两个隐藏字段。DB_ROW_ID 也是 InnoDB 的内部字段,但主要用于特定情况下生成聚簇索引,不参与版本可见性判断。

2.3 Read View 的可见性规则

Read View 主要包含以下信息:

字段 含义
creator_trx_id 创建读视图的事务 ID
m_ids 创建读视图时仍然活跃的读写事务 ID 集合
min_trx_id 活跃事务 ID 的最小值;没有活跃事务时取上界值
max_trx_id 创建读视图时下一个待分配的事务 ID

这里的 min_trx_id、max_trx_id 是便于理解的名称,分别对应 InnoDB 内部的 m_up_limit_id、m_low_limit_id。

判断规则如下:

  1. 当前事务自己的修改:可见。

  2. 版本事务 ID 小于 min_trx_id:说明该事务在创建读视图之前已经提交,可见。

  3. 版本事务 ID 大于等于 max_trx_id:说明对应事务在创建读视图时尚未分配该 ID,不可见。

  4. 版本事务 ID 处于两个边界之间:如果在 m_ids 中,说明当时还未提交,不可见;如果不在,说明创建读视图时已经提交,可见。

如果版本不可见,就沿 Undo Log 版本链继续寻找。

核心依据是事务在 Read View 创建时的状态,以及当前事务自身的修改,不能只根据事务 ID 大小判断是否已经提交。

3. 高频面试追问

3.1 读已提交(RC)和可重复读(RR)中的 MVCC 有什么区别?

核心区别是 Read View 的创建和复用时机。

在读已提交(Read Committed,RC)隔离级别下,每次执行一致性读都会创建新的 Read View,因此可以看到其他事务在本次查询开始前已经提交的修改。

在可重复读(Repeatable Read,RR)隔离级别下,事务第一次执行一致性读时创建 Read View,后续一致性读复用同一个 Read View,因此通常能够保持相同的查询结果。

例如,事务 A 第一次查询余额为 100,事务 B 将余额修改为 200 并提交:

  • RC:事务 A 再次查询,可以看到 200。

  • RR:事务 A 再次进行普通快照读,仍然看到 100。

另外,当前事务自身的修改在两种隔离级别下都可以被后续查询看到。

继续追问:RC 下一条 SELECT 执行过程中,其他事务提交的数据会立即可见吗?

不会。普通一致性读使用本次查询开始时建立的快照。其他事务在查询执行过程中提交的修改,通常要等到下一次一致性读创建新快照后才能看到。

3.2 RR 隔离级别下,执行 BEGIN 就会创建 Read View 吗?

一般不会。

在 InnoDB 默认的 RR 隔离级别下,普通 BEGIN 只是开启事务,Read View 通常在第一次执行一致性读时创建。

例如:

事务 A 执行 BEGIN → 事务 B 修改数据并提交 → 事务 A 第一次执行普通 SELECT。

此时事务 A 通常能够看到事务 B 已提交的修改,因为 Read View 是在事务 B 提交之后才创建的。

如果使用 START TRANSACTION WITH CONSISTENT SNAPSHOT,则可以在事务启动时立即建立一致性快照,主要用于 RR 隔离级别下需要固定读取时间点的场景。

3.3 快照读和当前读有什么区别?

快照读通过 MVCC 读取符合 Read View 可见性规则的数据;当前读需要读取并锁定当前有效的数据版本。

快照读的典型操作是普通 SELECT。在 RC 和 RR 隔离级别下,通常使用一致性读,不会给读取到的记录加行锁。

当前读主要包括:

  • SELECT ... FOR UPDATE

  • SELECT ... FOR SHARE

  • UPDATE

  • DELETE

这些操作需要获取相关锁。如果目标记录正被其他事务修改,可能等待对方提交或回滚。

例如,查询订单详情可以使用普通 SELECT;如果需要先检查订单状态,再修改订单,就可以在事务中使用 SELECT ... FOR UPDATE 锁定相关记录。

3.4 为什么 RR 隔离级别下,SELECT 读到旧数据,UPDATE 却能修改新数据?

因为普通 SELECT 使用快照读,而 UPDATE 处理当前有效的数据版本。

假设事务 A 第一次查询余额是 100,建立了 Read View。

随后事务 B 把余额修改成 200 并提交。

这时事务 A 再次执行普通 SELECT,仍然看到 100。

但如果事务 A 执行:

UPDATE account SET balance = balance + 10 WHERE id = 1;

UPDATE 会基于当前有效的余额 200 进行修改,最终变成 210。

而且事务 A 后续执行普通 SELECT,也能看到自身修改后的 210。

因此,RR 保证的是一致性读的快照规则,不代表事务中的所有操作始终使用同一个历史版本。

3.5 MVCC 能完全解决幻读吗?

MVCC 可以避免 RR 隔离级别下连续快照读出现幻读,但处理当前读的幻读还需要锁机制。

幻读指的是同一事务两次执行相同条件的范围查询,因为其他事务插入或删除并提交记录,导致结果集发生变化。

InnoDB 在 RR 隔离级别下主要通过两种机制处理:

  • 快照读:复用同一个 Read View,其他事务在快照建立后插入的记录不会突然出现在后续快照读中。

  • 当前读:针对需要锁定索引范围的查询,使用间隙锁或临键锁(Next-Key Lock),限制其他事务向相关范围插入记录。

Next-Key Lock 可以理解为记录锁与间隙锁的组合。

不过,如果同一事务先执行快照读,再执行当前读,仍然可能观察到不同的数据结果,因为两种读取方式采用的可见性规则不同。

所以,不能认为只要使用 RR 和 MVCC,所有查询方式下的幻读问题都会自动消失。

3.6 MVCC 能防止丢失更新吗?

MVCC 本身无法保证所有读后写操作的正确性,还需要配合锁、条件更新或乐观锁。

例如,账户余额为 100,两个事务都通过快照读获取余额,然后分别计算扣减 10 元,得到 90。

如果两个事务最终都执行 SET balance = 90,那么最终余额可能是 90,发生丢失更新。

实际项目中可以采用三种方式:

  1. 悲观锁:使用 SELECT ... FOR UPDATE 锁定记录,再计算和修改。

  2. 乐观锁:通过版本号判断数据是否被其他事务修改,更新失败后重试。

  3. 原子条件更新:直接在 SQL 中进行运算,同时检查业务条件。

例如:

UPDATE account
SET balance = balance - 10
WHERE id = 1 AND balance >= 10;

这条语句通过数据库的并发更新机制和余额条件控制扣减,再根据影响行数判断是否成功。

对于库存扣减、余额修改等场景,通常优先考虑原子条件更新;涉及多条记录或复杂业务校验时,可以使用事务和行锁。

继续追问:乐观锁为什么能够避免覆盖其他事务的更新?

乐观锁通常增加一个 version 字段,查询时获取当前版本号,更新时把版本号放进 WHERE 条件,并在成功更新后递增版本号。

如果其他事务已经修改过这条记录,版本号就会变化,当前更新影响行数为 0,可以据此重试或提示冲突。

3.7 有了 MVCC,为什么数据库还需要加锁?

因为 MVCC 主要解决一致性读与并发写入之间的冲突,写写冲突和部分业务一致性问题仍然需要锁机制。

例如,两个事务同时修改同一条库存记录,如果完全不控制写入顺序,就可能出现并发覆盖或业务状态错误。

InnoDB 使用行锁协调这些写操作,让相互冲突的修改按受控顺序执行。

另外,MVCC 允许不同事务读取不同的数据版本,但无法单独保证复杂业务约束。

因此,InnoDB 实际采用的是 MVCC 与锁机制协同工作的并发控制方案:

  • 普通一致性读主要依靠 MVCC。

  • 数据更新和加锁读取依靠锁协调冲突。

  • 范围写入等场景还可能使用间隙锁、临键锁保护相关索引范围。

3.8 长事务为什么会影响 MVCC 性能?

长事务可能使历史版本无法及时清理,导致 Undo Log 持续积累,增加存储和查询开销。

InnoDB 在修改数据之后,需要保留可能被旧事务读取的历史版本。

如果一个 RR 事务长期持有旧的 Read View,后台 Purge 线程就不能清理仍可能被该事务访问的历史版本。

这会带来几个影响:

  • Undo 历史记录持续增长。

  • 查询旧版本时可能需要沿版本链查找更多历史信息。

  • 后台清理滞后,增加存储和 I/O 压力。

实际项目中应尽量缩短事务持续时间,避免事务开启后长时间处理网络请求或等待外部服务,同时监控活跃事务和 History list length。

3.9 MySQL InnoDB 和 PostgreSQL 的 MVCC 实现有什么区别?

两者都通过多版本和快照控制数据可见性,但历史版本的组织方式不同。

MySQL InnoDB 主要在聚簇索引记录中维护当前数据,并通过 Undo Log 恢复历史版本。

PostgreSQL 更新记录时通常会生成新的行版本,旧版本保留在表存储中,通过 xmin、xmax 等事务信息判断可见性。

在历史版本清理方面:

  • MySQL InnoDB:主要通过 Purge 清理不再需要的 Undo 历史和已删除记录。

  • PostgreSQL:主要通过 VACUUM 回收无用行版本占用的空间。

两者的目标一致,都是提高并发读写能力,但具体存储结构、版本清理方式和隔离级别行为存在差异。

4. 易混淆概念对比

4.1 快照读与当前读

对比维度 快照读 当前读
读取内容 Read View 可见的版本 当前有效版本
是否加行锁 普通一致性读通常不加 需要获取相关锁
典型操作 普通 SELECT SELECT FOR UPDATE、UPDATE、DELETE
主要场景 普通业务查询 数据修改、并发控制

4.2 RC 与 RR

对比维度 RC(读已提交) RR(可重复读)
Read View 创建 每次一致性读 第一次一致性读,之后复用
脏读 避免 避免
不可重复读 可能出现 连续快照读可避免
快照读幻读 可能出现 连续快照读可避免
InnoDB 锁策略 一般减少间隙锁使用 范围加锁时通常使用临键锁
适用场景 更强调并发和读取较新数据 更强调事务内读取结果稳定

4.3 Undo Log 与 Redo Log

对比维度 Undo Log Redo Log
主要作用 事务回滚、恢复历史版本 崩溃恢复、保证持久性
记录内容 撤销修改所需的信息 重做修改所需的信息
与 MVCC 的关系 提供历史版本 不负责版本可见性判断
清理方式 版本不再被需要后逐步清理 按 Redo Log 的循环写入与检查点机制复用空间

4.4 MVCC 与锁机制

对比维度 MVCC 锁机制
核心思路 不同事务读取不同版本 协调冲突操作的执行顺序
主要优势 降低普通读写冲突 控制并发修改
适用操作 一致性读 写操作、加锁读
主要限制 无法单独解决所有并发写入问题 可能产生等待、死锁

4.5 不可重复读与幻读

对比维度 不可重复读 幻读
核心现象 同一条记录的值发生变化 相同条件查询的结果集发生变化
常见原因 其他事务更新并提交记录 其他事务插入或删除并提交记录
示例 两次查询余额分别为 100 和 200 两次查询满足条件的订单分别为 5 条和 6 条
RR 下的处理 固定快照保证连续一致性读 快照读依靠 MVCC,范围当前读结合锁机制

5. 总结

MVCC 是数据库用于提高并发性能的多版本并发控制机制。以 MySQL InnoDB 为例,它通过隐藏字段、Undo Log 和 Read View 实现数据版本管理与可见性判断,让不同事务能够读取符合自身快照的数据版本。它最大的优势是减少普通读写之间的锁冲突,并通过 Read View 的不同生成策略支持 RC 和 RR 隔离级别。不过,MVCC 主要解决一致性读问题,涉及写写冲突、库存扣减和范围并发修改时,仍然需要配合行锁、临键锁或乐观锁等机制保证数据正确性。