当前位置: 代码网 > it编程>数据库>Mysql > 浅谈MySQL InnoDB实现MVCC原理

浅谈MySQL InnoDB实现MVCC原理

2026年03月06日 Mysql 我要评论
核心价值先记住:mvcc 的终极目标是实现「读不加锁,读写互不阻塞」,极大提升数据库的并发读写性能,这也是 innodb 能替代 myisam 的核心原因之一。一、先搞懂:为什么需要 mvcc?(mv

核心价值先记住: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 个字段是可见性判断的全部依据,无任何多余字段:

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_id 且 trx_id ∈ m_ids→ 说明这个版本的数据是由「当前活跃的未提交事务」修改的,数据不可见;
  4. ✅ 规则 4:如果 min_trx_id ≤ trx_id < max_trx_id 且 trx_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 语句,这也是高级开发和架构师的必备功底。

到此这篇关于浅谈mysql innodb实现mvcc原理的文章就介绍到这了,更多相关mysql innodb mvcc内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com