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 → 判断版本可见性 → 返回符合条件的数据。
-
数据修改:事务更新记录时,InnoDB 修改当前记录,并通过 Undo Log 保留恢复旧版本所需的信息。
-
构建版本链:当前记录通过回滚指针关联历史版本,形成可供追溯的版本链。
-
创建 Read View:执行一致性读时,按照事务隔离级别创建或复用 Read View,记录用于判断事务可见性的信息。
-
判断可见性:比较记录的事务 ID 和 Read View。如果当前版本可见,就直接返回;否则继续查找更早的版本。
-
返回数据:找到第一个可见版本后返回。对于当前快照不可见的新增记录,查询结果中不会出现该记录。
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。
判断规则如下:
-
当前事务自己的修改:可见。
-
版本事务 ID 小于
min_trx_id:说明该事务在创建读视图之前已经提交,可见。 -
版本事务 ID 大于等于
max_trx_id:说明对应事务在创建读视图时尚未分配该 ID,不可见。 -
版本事务 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,发生丢失更新。
实际项目中可以采用三种方式:
-
悲观锁:使用
SELECT ... FOR UPDATE锁定记录,再计算和修改。 -
乐观锁:通过版本号判断数据是否被其他事务修改,更新失败后重试。
-
原子条件更新:直接在 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 主要解决一致性读问题,涉及写写冲突、库存扣减和范围并发修改时,仍然需要配合行锁、临键锁或乐观锁等机制保证数据正确性。