MySQL InnoDB 实现 MVCC 原理

核心价值先记住:MVCC 的终极目标是实现「读不加锁,读写互不阻塞」,极大提升数据库的并发读写性能,这也是 InnoDB 能替代 MyISAM 的核心原因之一。

一、先搞懂:为什么需要 MVCC?(MVCC 的诞生意义)

在 MVCC 出现之前,数据库的并发控制只有两种方式,都有致命缺陷:

  1. 加锁查询:读操作加共享锁,写操作加排他锁 → 读和写互相阻塞,并发性能极低;
  2. 无锁查询 :不加锁直接读 → 会出现脏读、不可重复读、幻读等事务隔离性问题,数据一致性无法保证。

MVCC 完美解决了这个矛盾

MVCC 是一种无锁的并发控制机制 ,对读操作(普通 SELECT)完全不加锁 ,对写操作只加行级锁;读操作不会阻塞写操作,写操作也不会阻塞读操作,同时还能保证不同事务的隔离性,精准解决脏读、不可重复读、幻读问题。


二、MVCC 核心前置知识(必须掌握,3 个根基,缺一不可)

MVCC 的实现没有任何黑魔法 ,完全依赖 InnoDB 三个底层核心设计组合实现,这三个是理解 MVCC 的绝对前提,所有原理都是基于这三点展开:

✅ 根基 1:InnoDB 每行数据的「3 个隐藏字段」(版本核心标识)

InnoDB 存储引擎中,我们建表时定义的每一行数据,在磁盘实际存储时,都会自动额外添加 3 个隐藏字段 (无需手动定义,引擎自动维护),这是行数据的版本号核心标识,重中之重!

复制代码
每行数据的物理存储 = 我们定义的列 + 3个隐藏字段

三个隐藏字段的作用(MySQL 8.0/5.7 通用):

  1. db_trx_id 【6 字节】事务 ID当前行数据的最后一次修改 / 插入的事务 ID ,是自增的唯一值(事务开启时,InnoDB 会分配一个全局唯一的递增事务 ID);
    • 插入一行:该行的db_trx_id = 插入事务的 ID;
    • 更新一行:该行的db_trx_id = 更新事务的 ID(更新本质是「标记旧行删除 + 插入新行」);
    • 删除一行:该行的db_trx_id = 删除事务的 ID(删除本质是「标记删除」);
  2. db_roll_ptr 【7 字节】回滚指针 :指向当前行数据对应的 undo log 回滚日志 的地址,通过这个指针可以找到该行的「历史版本数据」;
  3. db_row_id 【6 字节】行 ID :可选隐藏字段,只有当表没有主键、也没有唯一非空索引 时,InnoDB 才会自动生成这个字段,作为聚簇索引。有主键的表不会生成这个字段

✅ 根基 2:Undo Log 回滚日志 & 版本链(历史版本的载体)

① 什么是 Undo Log

Undo Log(回滚日志)是 InnoDB 事务四大日志之一(redo/undo/binlog/error log),属于逻辑日志,作用有两个:

  • 事务回滚:事务执行失败时,通过 undo log 恢复数据到修改前的状态;
  • 支撑 MVCC:存储行数据的历史版本,供其他事务做「一致性读」。
② 版本链的形成(核心结构)

基于「隐藏字段db_roll_ptr + undo log」,InnoDB 会为每行数据生成一条 版本链,规则如下:

  1. 当事务对某行数据执行插入 / 更新 / 删除操作时,会先把该行数据的「旧版本」写入到 undo log 中;
  2. 该行数据的隐藏字段 db_roll_ptr 会指向这条 undo log 的地址;
  3. 如果该行数据被多次修改 ,则会生成多条 undo log,这些 undo log 通过 db_roll_ptr 指针首尾相连 ,形成一条版本链
  4. 版本链的头节点是数据的「最新版本」(存储在聚簇索引的叶子节点),版本链的后续节点是数据的「历史版本」(存储在 undo log 中)。

✨ 核心结论:版本链中,越往后的版本,事务 ID 越小(数据越旧)

版本链结构示意图(一目了然)
复制代码
【聚簇索引中存储的 最新版本数据】
行记录(最新):col1=1, col2=2 | db_trx_id=50 | db_roll_ptr → 指向undo log版本40
          ↑
          |
【undo log 中的 历史版本链】
undo log版本40:col1=1, col2=1 | db_trx_id=40 | db_roll_ptr → 指向undo log版本30
          ↑
          |
undo log版本30:col1=0, col2=1 | db_trx_id=30 | db_roll_ptr = null (最早版本)

✅ 根基 3:InnoDB 的「非锁定读」(MVCC 的读模式,核心)

InnoDB 对 SELECT 查询提供了两种读模式,MVCC 依赖的是非锁定读 ,这是 InnoDB 的默认读模式

  1. 锁定读select ... for update / select ... lock in share mode,会加行锁 / 共享锁,读写阻塞,一般用于写多读少场景;
  2. 非锁定读 :普通的 SELECT * FROM table完全不加锁 ,这是我们日常开发 99% 的查询方式,也是 MVCC 的核心载体;
    1. 核心逻辑:非锁定读时,InnoDB 会通过「版本链」读取行数据的某个历史版本,而不是最新版本,从而实现「读不加锁,读写不阻塞」。

三、MVCC 最核心的实现:ReadView(读视图,可见性规则)

面试必考核心:MVCC 的核心就是「版本链」 + 「ReadView」,版本链提供了数据的历史版本,ReadView 提供了版本的可见性判断规则。

✅ 3.1 什么是 ReadView(读视图)

ReadView 翻译成「读视图 / 一致性视图」,是事务在执行查询操作时,生成的一个「当前数据库中活跃事务的快照」

  • 「活跃事务」:指的是当前已经开启但还未提交的事务
  • 「快照」:生成后就不会再变,是一个静态的视图;
  • 生成时机:不同的事务隔离级别,生成 ReadView 的时机完全不同(这是解决脏读 / 不可重复读 / 幻读的关键,后文重点讲)。

✅ 3.2 ReadView 的 4 个核心字段(固定结构)

每个 ReadView 内部都维护了 4 个核心字段,这 4 个字段是可见性判断的全部依据,无任何多余字段:

java 复制代码
class ReadView {
    // 1. 当前系统中,所有「活跃事务」的事务ID集合(已开启未提交)
    private Set<Long> m_ids;
    // 2. 活跃事务中,最小的事务ID
    private Long min_trx_id;
    // 3. 生成该ReadView时,系统「下一个要分配的事务ID」(即当前最大的事务ID+1)
    private Long max_trx_id;
    // 4. 生成该ReadView的「当前事务」的事务ID
    private Long creator_trx_id;
}

✅ 3.3 【重中之重】行版本的「可见性判断规则」

这是 MVCC 的灵魂逻辑,也是面试的必考点,所有的隔离性保证都源于这套规则。

核心流程:事务执行普通 SELECT 时,生成 ReadView → 从版本链中读取行数据的版本 → 用这套规则判断「该版本的数据是否对当前事务可见」。

判断规则(对版本链中的某一行版本数据,依次判断,有一个满足即可) :假设 当前待判断的行版本的事务 ID = trx_id,当前 ReadView 的字段如上;

  1. 规则 1 :如果 trx_id < ReadView.min_trx_id→ 说明这个版本的数据是由「已经提交的事务」修改的,数据可见
  2. 规则 2 :如果 trx_id >= ReadView.max_trx_id→ 说明这个版本的数据是由「在当前事务开启后才启动的事务」修改的,数据不可见
  3. 规则 3 :如果 min_trx_id ≤ trx_id < max_trx_idtrx_id ∈ m_ids→ 说明这个版本的数据是由「当前活跃的未提交事务」修改的,数据不可见
  4. 规则 4 :如果 min_trx_id ≤ trx_id < max_trx_idtrx_id ∉ m_ids→ 说明这个版本的数据是由「在当前事务开启前已提交的事务」修改的,数据可见
✅ 不可见的处理逻辑

如果当前版本的数据不可见 ,则通过该行的 db_roll_ptr 指针,去版本链中读取上一个历史版本 ,然后重复执行上述 4 条规则,直到找到第一个可见的版本,如果版本链遍历完都没有可见版本,则返回空。


四、【核心面试考点】不同隔离级别下 MVCC 的实现差异

面试必问的:为什么 RC 能解决脏读,RR 能解决不可重复读和幻读?本质是「生成 ReadView 的时机不同」

前提:MySQL 默认事务隔离级别是 RR(可重复读) ,另一个常用级别是 RC(读已提交) ;这两个级别都基于 MVCC 实现,而「读未提交 / 串行化」不依赖 MVCC。

✅ 核心结论(先记死,面试必答)

  1. RC(读已提交)每次执行 SELECT 查询时,都会生成一个新的 ReadView
  2. RR(可重复读)事务中第一次执行 SELECT 查询时,生成唯一的一个 ReadView,之后整个事务的所有查询都复用这个 ReadView

✅ 4.1 案例 1:RR(可重复读)的实现过程(解决不可重复读)

场景模拟
  • 事务 A(trx_id=10):隔离级别 RR,执行查询 SELECT name FROM user WHERE id=1
  • 事务 B(trx_id=20):开启事务,更新 user 表 id=1 的 name 为 "李四",但未提交
  • 事务 C(trx_id=30):开启事务,更新 user 表 id=1 的 name 为 "王五",提交事务
执行流程
  1. 事务 A 第一次执行 SELECT → 生成唯一的 ReadView
    • m_ids={20}(事务 B 活跃未提交)、min_trx_id=20、max_trx_id=31、creator_trx_id=10;
  2. 读取行数据最新版本,trx_id=30(事务 C 提交),根据规则判断:30 <31 且 30 ∉ {20} → 可见,返回 name="王五";
  3. 此时事务 B 提交(trx_id=20),事务 A再次执行相同的 SELECT复用第一次的 ReadView,判断规则不变;
  4. 即使数据有新的版本,事务 A 读取的结果还是 name="王五",两次查询结果一致 → 解决了「不可重复读」。

✅ 4.2 案例 2:RC(读已提交)的实现过程(存在不可重复读)

同样的场景,事务隔离级别改为 RC:

  1. 事务 A 第一次执行 SELECT → 生成 ReadView1,判断后返回 name="王五";
  2. 事务 B 提交后,事务 A再次执行 SELECT生成新的 ReadView2,此时 m_ids 为空,min_trx_id=31;
  3. 读取最新版本数据,trx_id=20 < 31 → 可见,返回 name="李四";
  4. 两次查询结果不一致 → 存在「不可重复读」,但解决了「脏读」。

✅ 为什么 RR 能解决幻读?

RR 级别下,因为整个事务复用同一个 ReadView,所以无论其他事务插入 / 删除多少数据,当前事务都看不到,因为新插入的数据的 trx_id >= max_trx_id,永远不可见 → 完美解决「幻读」。


五、补充:MVCC 对 DELETE/INSERT 的处理逻辑

✅ 1. DELETE 操作

InnoDB 中没有真正的物理删除 ,执行 DELETE 时,只是给该行数据打上一个「删除标记」,并把该行的 db_trx_id 更新为删除事务的 ID;

  • 对其他事务来说,通过可见性规则判断,这个被标记的版本是不可见的,就相当于「删除了」;
  • 物理删除是在后续的「垃圾回收(purge)」阶段,由 InnoDB 后台线程清理掉这些不可见的版本。

✅ 2. INSERT 操作

插入一行数据时,会生成一条新的版本链头节点,该行的 db_trx_id 是插入事务的 ID;

  • 未提交的插入,对其他事务不可见;提交后的插入,对其他事务是否可见,依然遵循可见性规则。

✅ 3. UPDATE 操作

InnoDB 中更新本质是「写时复制」 :执行 UPDATE 时,不会直接修改原行数据,而是:

  1. 把原行数据的旧版本写入 undo log,形成版本链;
  2. 插入一条新的行数据(新版本),更新 db_trx_id 为当前事务 ID;
  3. 原行数据被标记为「删除状态」,后续由 purge 线程清理。

六、MVCC 的优缺点 & 适用场景(面试加分)

✅ 优点(为什么 MVCC 是最优解)

  1. 极致的并发性能:读不加锁,读写互不阻塞,这是最大的优势,高并发场景下性能碾压加锁查询;
  2. 保证事务隔离性:RC/RR 级别下,完美解决脏读、不可重复读、幻读(RR),兼顾性能和一致性;
  3. 无锁开销:不需要维护锁的申请、释放、等待,减少了数据库的锁竞争和上下文切换开销;

✅ 缺点(MVCC 的代价)

  1. 存储开销:需要存储 undo log 和版本链,会占用额外的磁盘空间;
  2. CPU 开销:查询时需要遍历版本链 + 执行可见性判断,有少量的 CPU 计算开销;
  3. 清理开销:InnoDB 需要后台 purge 线程清理过期的 undo log 版本,有一定的后台开销;

✅ 适用场景

99% 的业务场景都适用 MVCC,尤其是:

  • 读多写少的高并发场景(如电商列表、资讯详情、后台报表);
  • 不需要实时读取最新数据,能接受短时间数据一致性的场景;

例外:如果是写多读少,且要求实时读取最新数据(如金融转账、库存扣减),建议用锁定读。


七、MVCC 核心知识点总结(面试必背清单,精华无冗余)

  1. MVCC 的全称是多版本并发控制,是 InnoDB 的无锁并发控制机制,核心目标是读不加锁,读写互不阻塞
  2. MVCC 的实现依赖三个核心:行的 3 个隐藏字段、undo log 版本链、ReadView 读视图
  3. 每行数据的 db_trx_id 是最后一次修改的事务 ID,db_roll_ptr 指向 undo log 形成版本链;
  4. ReadView 是事务查询时的活跃事务快照,包含 4 个核心字段,是版本可见性的判断依据;
  5. 可见性判断规则是 MVCC 的灵魂,核心是判断行版本的 trx_id 和 ReadView 的关系;
  6. RC 和 RR 的核心差异是生成 ReadView 的时机不同:RC 每次查询生成新的,RR 事务内复用一个;
  7. RR 能解决幻读的本质是:事务内复用同一个 ReadView,看不到其他事务的插入 / 删除;
  8. InnoDB 的 delete 是逻辑删除,update 是写时复制,都依赖版本链实现;
  9. MVCC 只适用于 RC/RR 隔离级别,读未提交和串行化不依赖 MVCC。

最终总结

MVCC 不是一个单一的技术,而是 InnoDB 把「隐藏字段、版本链、undo log、ReadView」这几个底层设计精妙组合的产物 ,它的核心思想是:通过为每行数据维护多个版本,让读操作可以读取历史版本,从而避免加锁,实现读写并发

理解 MVCC 的原理,不仅能回答面试中的核心问题,更能让你在实际开发中,针对不同的业务场景选择合适的事务隔离级别和查询方式,写出高性能的 SQL 语句,这也是高级开发和架构师的必备功底。

相关推荐
ss2732 小时前
ruoyi 新增每页分页条数
java·数据库·mybatis
万粉变现经纪人2 小时前
如何解决 pip install mysqlclient 报错 ‘mysql_config’ not found 问题
数据库·python·mysql·pycharm·bug·pandas·pip
lkbhua莱克瓦242 小时前
进阶-SQL优化
java·数据库·sql·mysql·oracle
石小千2 小时前
Myql binlog反向解析成sql
数据库·sql
alonewolf_992 小时前
MySQL 8.0 主从复制原理深度剖析与实战全解(异步、半同步、GTID、MGR)
数据库·mysql·adb
八九燕来3 小时前
django + drf 多表关联场景下的序列化器选型与实现逻辑
数据库·django·sqlite
Mr. Cao code3 小时前
MySQL数据卷实战:持久化存储秘籍
数据库·mysql·docker·容器
小北方城市网3 小时前
微服务架构设计实战指南:从拆分到落地,构建高可用分布式系统
java·运维·数据库·分布式·python·微服务