深蓝黑背景品牌封面:居中大标题「隔离级别和 MVCC,一题讲透」,副标题「四个隔离级别 × 一条版本链」;下方装饰——一行数据的三个历史版本 V1、V2、V3 由回滚指针串成链,旁边一只「读视图」眼睛图标指向被选中的版本;顶部标注「经典题解析 · 数据库」与「Day 06 · 09-24」 图 1:本文封面,点明「四个隔离级别」与「一条版本链」两条主线。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料(见文末来源说明),与任何公司的当前招聘无关。「四种隔离级别」是数据库面试的必背题,但只背表格的人过不了追问——面试官真正想要的后半句是:InnoDB 的「可重复读」为什么能做到读写互不阻塞?这就必须讲 MVCC。

想象面试现场,面试官问:「数据库的隔离级别有哪几种?分别解决什么问题?MVCC 是怎么回事?」

第一步,审题。

这题是上下两集:上集讲四个隔离级别分别防住什么异常,下集讲 InnoDB 用什么机制实现它们。两集之间有一根暗线——隔离性每升一级,并发度就降一截,MVCC 的价值恰恰是在不太牺牲并发的前提下把隔离性做高。

第二步,上集:四个级别与三种异常。

先定义三种读异常:脏读(读到别人还没提交的数据)、不可重复读(同事务内两次读同一行结果不同)、幻读(同事务内两次按条件查询,结果集条数变了)。

对照 MySQL 8.4 官方文档逐级过一遍。读未提交:SELECT 可能用到一行的「更早或更晚」版本,官方直接称之为脏读,实际无人使用。读已提交:每一次一致性读都建立自己的新快照,别人提交了你立刻可见——脏读解决了,但同一事务里两次读可能不一样,不可重复读仍在;文档还点明这一级间隙锁被禁用,幻读可能发生。可重复读:InnoDB 的默认级别,同一事务内的一致性读都读第一次读时建立的快照,多次读结果天然一致;文档同时说明,范围条件会用间隙锁或 next-key 锁挡住别的事务往间隙里插行,这正是 InnoDB 在这一级就压住幻读的手段。串行化:相当于可重复读,但在关闭自动提交时会把普通 SELECT 隐式转成 SELECT ... FOR SHARE——读写开始排队,隔离最高、并发最低。

第三步,下集:MVCC 的版本链与读视图。

MVCC 的底座藏在每行数据里。官方文档写明 InnoDB 给每行加了三个隐藏字段:6 字节的 DB_TRX_ID 记录最后插入或更新该行的事务号,7 字节的 DB_ROLL_PTR 回滚指针指向 undo 日志里重建旧版本所需的信息,6 字节的 DB_ROW_ID 是单调递增的行号。更新一行时,旧版本不进坟墓进 undo 日志,由回滚指针一串串成版本链;删除在内部也被当作一种「打了特殊标记的更新」。

读的时候,事务先建立一个读视图(快照),再按事务号沿版本链挑一个对自己可见的版本——比自己新的版本跳过,顺着回滚指针往下找。写走当前版本,读走历史快照,两条路互不阻塞,这就是「可重复读」高并发的底气。

左半部分为四级隔离阶梯,自下而上隔离性增强、并发度降低:读未提交(可能读到别人没提交的数据,即脏读)、读已提交(只读已提交数据,解决脏读)、可重复读(InnoDB 默认,同事务内多次读结果一致,解决不可重复读)、串行化(读写排队,解决幻读但并发最低)。右半部分为 MVCC 原理:每行数据带隐藏列 trx_id 与 roll_pointer,旧版本存在 undo 日志里串成版本链;事务读数据时先建读视图,按事务号挑一个对自己可见的版本。底部结论:MVCC 用版本链加读视图,让读写互不阻塞——这是「可重复读」高并发的底气 图 2:四级隔离阶梯与 MVCC 版本链、读视图的工作方式。

第四步,一个必须知道的生产代价。

版本链不是免费的。旧版本要等「不再有任何事务的快照可能需要它」之后才能被清理(purge)。官方文档因此明确建议:事务要及时提交,包括只做一致性读的事务——否则 undo 日志无法丢弃,回滚段会越涨越大,直至占满 undo 表空间。长事务是 MVCC 的头号天敌,这句话在生产排障时比背定义值钱。

易错点清单。

一,把「可重复读解决幻读」说成所有数据库的通例——这是 InnoDB 靠快照读加 next-key 锁的组合拳做到的,标准里这一级并不保证。二,以为读已提交也用同一个快照——文档写明它每次读都建新快照,这正是两级行为的差别根源。三,讲 MVCC 只讲「每行存多个版本」,讲不出 DB_TRX_ID、回滚指针和读视图这老三样。四,忘了长事务撑爆 undo 的代价——只讲收益不讲成本,答案就不完整。

回到面试现场,建议的答题节奏:先报四种异常与四个级别的对应表,再点明 InnoDB 默认可重复读,然后展开 MVCC 三件套(隐藏字段、版本链、读视图),最后补长事务与 purge 的收尾。上半考记忆,下半考理解,节奏别反。

标注平台、实验管理这类多人并发写库的场景,隔离级别选错就是数据事故。你排查过最诡异的并发读问题是什么——脏读、不可重复读,还是幻读?欢迎分享。

参考资料

  • InnoDB Transaction Isolation Levels(MySQL 8.4 官方文档:可重复读为 InnoDB 默认级别、读已提交每次读建新快照、读未提交即脏读、串行化把普通 SELECT 隐式转为 FOR SHARE、间隙锁与 next-key 锁行为) — https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html
  • InnoDB Multi-Versioning(MySQL 8.4 官方文档:DB_TRX_ID/DB_ROLL_PTR/DB_ROW_ID 三个隐藏字段及字节数、undo 日志重建旧版本、删除作为带标记的更新、purge 清理与「事务应及时提交否则回滚段膨胀」的建议) — https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html