YAOTU INSIGHTS

MySQL InnoDB MVCC 深度解析

MySQL InnoDB MVCC 深度解析
MVCC 是 InnoDB 实现高并发事务的核心也是 Java 后端面试高频压轴题。很多人只记住一句话 “多版本并发控制读写不阻塞”但一问底层隐藏字段、undo log 版本链、ReadView 可见性规则、RC 与 RR 的区别、MVCC 到底能不能解决幻读就容易翻车。本文从原理、示例、踩坑点、面试追问完整拆解。前言在数据库并发场景中如果使用锁机制读加共享锁、写加排他锁会造成读写互相阻塞并发性能很差。 MVCCMulti-Version Concurrency Control多版本并发控制简单一句话保存数据的多个历史版本实现不加锁的快照读读写互不阻塞大幅提升并发能力。MVCC 只在 InnoDB 的READ COMMITTED读已提交、REPEATABLE READ可重复读MySQL 默认两个隔离级别生效。 读未提交Read Uncommitted不需要 MVCC串行化Serializable直接加锁不使用 MVCC。两种读快照读一致性非锁定读普通select查询走 MVCC不加行锁读取历史版本。当前读锁定读select ... for update、select ... lock in share mode、update、delete、insert读取最新数据会加行锁不走 MVCC 快照。一、MVCC 底层三大基石隐藏字段、undo log、版本链1. 行记录隐藏字段InnoDB 的每一行数据除了你定义的业务字段默认自带 3 个隐藏列隐藏字段作用DB_TRX_ID6 字节最近修改这条记录的事务 ID。新增 / 更新数据时填充当前事务 IDDB_ROLL_PTR7 字节回滚指针指向 undo log 里的旧版本数据用来串联版本链DB_ROW_ID6 字节行 ID如果表没有主键InnoDB 自动生成聚簇索引有主键时该字段无效每次更新数据不会直接覆盖旧数据而是将旧数据拷贝到 undo log修改当前行DB_TRX_ID为本事务 IDDB_ROLL_PTR指向 undo log 里旧版本记录。2. undo log 与版本链undo log回滚日志保存数据修改前的快照版本。版本链同一行数据多个历史版本通过 roll_ptr 串成单向链表。举个例子 初始数据name张三事务 ID100 插入。事务 200 更新 name 为 张三 A → 旧版本张三写入 undo log当前行 roll_ptr 指向版本 100。事务 300 更新 name 为 张三 B → 旧版本张三 A写入 undo logroll_ptr 指向版本 200。版本链当前最新记录(300) → undo版本(200) → undo版本(100)undo log 不仅仅用于事务回滚MVCC 的历史快照就是从 undo log 版本链读取。 事务提交后undo log 不会立刻删除当没有事务再需要读取这个版本时才会被 purge 线程清理。二、ReadView读视图版本可见性判断规则ReadView 是 MVCC 的核心相当于一个快照用来判断版本链里的这条历史记录对当前事务是否可见。ReadView 包含 4 个核心属性m_ids当前生成 ReadView 那一刻所有活跃未提交事务 ID 集合min_trx_id活跃事务最小 IDmax_trx_id下一个将要分配的事务 ID活跃事务最大 ID1creator_trx_id创建这个 ReadView 的当前事务 ID可见性判断逻辑重点面试必考拿到版本链中某一条记录的DB_TRX_ID依次判断如果DB_TRX_ID min_trx_id修改该数据的事务在 ReadView 生成前已经提交可见。如果DB_TRX_ID max_trx_id修改该数据的事务在 ReadView 之后才开启不可见。如果min_trx_id DB_TRX_ID max_trx_id需要判断这个 trx_id 是否在m_ids活跃集合在 m_ids事务还没提交 →不可见不在 m_ids事务已经提交 →可见如果记录DB_TRX_ID creator_trx_id是当前事务自己修改的数据可见。如果当前版本不可见就顺着 roll_ptr 指针去版本链找上一个历史版本重复上面判断直到找到可见版本或者版本链遍历结束。三、RC 和 RR 隔离级别ReadView 生成时机不同最本质区别RC读已提交每一次普通 select 快照读都会生成全新 ReadView每次查询都获取最新活跃事务列表所以可以读到别的事务已经提交的数据会出现不可重复读。RR可重复读MySQL 默认事务第一次快照读的时候生成 ReadView整个事务复用同一个 ReadView后续所有 select 不再新建 ReadView。所以在整个事务期间看到的快照是固定的不会读到其他事务提交的修改解决不可重复读。一句话区分 RC/RRReadView 什么时候创建。四、重点难点MVCC 能不能解决幻读面试高频坑结论快照读场景下RR 级别MVCC 通过固定 ReadView当前事务查询看不到其他事务新增的数据看起来没有幻读只是看不见不是阻止其他事务插入。当前读场景下MVCC 无效InnoDB 依靠临键锁Next-Key Lock来防止幻读。很多人踩坑MVCC 本身不能阻止幻读只是快照读屏蔽了幻读现象真正物理层面防止幻读靠行锁 间隙锁组成的临键锁。举个场景 事务 A 开启 RR第一次 select 没查到 id10 数据 事务 B 插入 id10 并且提交 事务 A 继续快照读还是看不到 id10感觉没有幻读 事务 A 执行select ... for update当前读立刻查到 id10 这条新数据幻读出现。所以面试回答一定要分快照读、当前读两个场景不要简单一句话 “MVCC 解决幻读”五、MVCC 的优缺点优点快照读不加锁读写不互相阻塞大幅提升并发性能实现事务隔离RC、RR 隔离级别依靠 MVCC 实现。缺点需要维护 undo log 版本链占用存储空间版本链过长purge 清理压力大查询时遍历版本链会降低性能不能覆盖当前读场景当前读仍然需要加锁长事务会阻止 undo log 清理导致 undo 表空间持续膨胀。生产环境大坑长事务。事务长时间不提交版本链无法被 purge 回收undo 日志越来越大磁盘占用飙升。线上要监控长事务。六、实战案例演示RR 级别事务 Atrx_id100开启事务第一次 select生成 ReadView事务 Btrx_id200开启事务修改数据并 commit事务 A 继续 select复用旧 ReadView看不到事务 B 提交的数据 → 可重复读。七、面试小整理Q1MVCC 的实现依赖什么A行隐藏字段、undo log 版本链、ReadView 读视图。Q2RC 和 RR 的 MVCC 区别ARC 每次 select 生成 ReadViewRR 事务第一次 select 生成全程复用。Q3快照读和当前读有哪些 SQLA快照读普通 select当前读update/delete/insert、select ... for update、lock in share mode。Q4长事务为什么会导致 undo 日志膨胀A长事务一直持有旧 ReadViewundo log 里的历史版本无法被 purge 线程回收版本链持续堆积。Q5MVCC 可以解决幻读吗A快照读下MVCC 屏蔽幻读现象当前读 MVCC 无效依靠临键锁解决幻读。结语MVCC 不是简单的 “快照”是 InnoDB 高并发事务的底层基石。很多开发只停留在背诵概念一旦区分快照读、当前读、RC/RR、幻读就容易混淆。 面试时讲清楚隐藏字段、版本链、ReadView 可见性规则再讲清楚幻读的两层含义会拉开和普通候选人差距。在生产开发中理解 MVCC 也能帮助我们分析长事务、慢查询、undo 日志膨胀等线上问题。