ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL MVCC底层原理与面试通关指南

MySQL MVCC底层原理与面试通关指南 先讲一个我印象特别深的场景。一个朋友去面后端岗位前面Java基础、项目聊得都挺好结果面试官话锋一转“谈谈你对MySQL MVCC的理解。”他下意识就来了一句“MVCC就是多版本并发控制能让读写不互斥。”面试官听了点点头接着又丢出两个问题“那RR隔离级别下ReadView什么时候生成版本链里的隐藏列到底有哪几个”气氛瞬间凝固最后他只能尴尬地说了句“这块记不太清了”。走出面试间他就知道那句“回去等消息”基本就是客气话。其实MVCC这个考点在MySQL面试里的出现频率极高但很多人对它的理解停留在“多版本并发控制”这八个字上。面试官既然专门问MVCC心里其实有一套考察路径先看你知不知道概念再看你能不能讲出隐藏列、undo log版本链、ReadView这些核心零件最后看你能否把隔离级别和快照读、当前读串联起来。只背第一层基本就是起步价过不了关也正常。这篇东西我准备把MVCC从底到顶完整拆一遍结合我自己的面试回答结构和线上排查经验说清楚每个概念“为什么存在”每步判断“依据是什么”最后再给你一套能直接用的答题框架。无论你是刚接触MySQL的新手还是准备跳槽的Java后端都能从这里拿到真正的干货。1. 面试被问MVCC光会说“了解”为什么等于白给1.1 面试官真正想考察的是你的“全链路理解”很多人以为面试官问MVCC就是想听一个名词解释。实际上在资深面试官眼里MVCC是一个天然的“综合题”它连接了MySQL的隔离级别、InnoDB存储引擎、undo log、行锁机制、事务可见性判断等多块知识。我问过一些参与招聘的后端负责人他们普遍的看法是一个人如果能主动把MVCC讲清楚至少说明他看过MySQL的底层实现而不是只会在业务代码里写SELECT和UPDATE。你回答“MVCC是多版本并发控制让读写不互相阻塞”这句话本身没毛病但它只证明你见过这个词。面试官接下来追问ReadView的生成时机、隐藏列字段、purge线程的作用就是在试探你对底层机制的掌握边界。答不上来自然会被判定为“背书型选手”。反过来想如果面试官只想考SQL语法根本不会去问MVCC。他问这个是因为事务并发场景在高并发系统里太常见了他希望招进来的人在未来处理死锁、数据不一致、性能抖动时具备快速定位根因的能力。这种能力的前提就是你对MVCC的认知不只停留在字面意思上。1.2 先给自己定位你对MVCC的掌握到底在哪个层级我把见过的人按理解深度分了四层你可以对照一下自己现在的位置。第一层是名词层能说出MVCC的全称是Multi-Version Concurrency Control知道它的作用是解决读写互斥到这个层面最多撑过第一个追问。第二层是概念层知道InnoDB的MVCC依赖隐藏列和undo log版本链明白通过版本链可以把旧版本数据保留下来给事务读取到这个层面能撑到第二轮追问。第三层是原理层理解ReadView的数据结构和可见性判断规则知道快照读与当前读的区别能说清楚RR和RC下ReadView生成时机的差异到这个层面大多数基础面试官已经满意。第四层是全景层除了上面的内容还能讲清undo log在事务回滚和MVCC里的双重角色、purge线程的清理策略、MVCC与锁共同作用如何撑起隔离级别。如果你能到这层别说面试线上出问题你也能自信地定位。说实话很多人面试时卡住不是脑子不够用而是知识都在“知道”层面没有串成线。下面我按面试官最想听到的逻辑把MVCC从零件到整体完整梳理一遍。2. MVCC的四个地基概念每个都要讲到骨子里2.1 隐藏列每行记录里藏着的“三件套”InnoDB引擎里每一行数据除了你定义的字段之外还有几个隐藏字段以MySQL 8.0为例最核心的三个是DB_TRX_ID、DB_ROLL_PTR以及DB_ROW_ID。DB_TRX_ID表示最近一次对该记录执行修改INSERT、UPDATE、DELETE的事务ID事务ID自增且全局唯一行记录被谁改过这个字段就记着谁。DB_ROLL_PTR是回滚指针它指向这条记录在undo log里的上一个版本。你想象一整行数据是一个版本节点这个指针负责把一个个节点串成链表也就是版本链。每做一次更新旧版本不会直接删除而是被指针链接起来新版本接管当前数据。DB_ROW_ID是隐藏主键它只在表中没有显式主键、也没有唯一键的时候出现对应InnoDB内部生成的6字节自增ID。如果表有主键这个字段就不会用到。我习惯用一个类比来记这三者的关系数据库里的一行记录就像Git仓库里的一个文件DB_TRX_ID记录“最后一次是谁提交的”DB_ROLL_PTR像commit历史指针一样指向“上一个提交版本”DB_ROW_ID则是当有人忘记建主键时InnoDB帮你自动补的一个文件编号。面试时提到隐藏列重点不是背名字而是说清楚“为什么需要DB_TRX_ID和DB_ROLL_PTR”。这两个字段一个用来判断事务可见性一个用来找旧版本MVCC能跑起来全靠它们支撑。2.2 undo log版本链记录的前世今生全在这条链上undo log在InnoDB里有两个作用事务回滚时恢复数据以及MVCC读新版本时回退数据。当执行UPDATE时InnoDB不会直接覆盖物理数据完事而是先把当前版本的数据写入undo log生成一个新的回滚段记录然后再更新聚簇索引里的记录行。与此同时新行上的DB_ROLL_PTR会指向刚写入的那个旧版本形成一条单向链表。链头是最新版本越往链尾走数据越老。DELETE比较特殊它本质上也是一次更新。InnoDB不会立即物理删除行而是给记录打上删除标记并把主键值等旧内容写入undo log让删除前的版本保留在版本链里。真正物理删除要等purge线程确认没有任何事务需要访问旧版本后才会执行。我见过很多新人把undo log理解成“一条文本日志”这是不对的。undo log里保存的是行记录的完整前镜像并且按事务和回滚段组织起来是支持MVCC读取的历史数据源不是简单的事务操作记录。你在系统里看到undo表空间不断变大往往就是长事务拖住了purge线程导致大量旧版本没法及时清理。版本链这个概念面试中几乎必问。能讲清“DB_ROLL_PTR如何串起历史版本”并且顺带解释purge线程的延迟影响会让面试官觉得你真的在线上处理过数据膨胀问题而不是只看过文档。2.3 ReadView判定数据可见性的“时间快照”ReadView是MVCC实现快照读时生成的一个视图理解为“事务在某个时间点对整个活跃事务空间的快照”就可以了。ReadView里核心维护四个信息需要牢牢记住creator_trx_id创建这个ReadView的事务ID也就是当前正在执行查询的事务自己。m_ids生成ReadView的瞬间系统中所有“活跃”事务ID列表。活跃的意思是还没提交包括正在运行中的和启动了但还没结束的。min_trx_idm_ids列表里最小的那个事务ID也就是当前活跃事务中“最老”的一个。max_trx_id生成ReadView瞬时的下一个事务ID注意它不等于某个已存在事务的ID而是一个“边界值”。所有事务ID小于它的才可能在ReadView生成前已提交大于等于它的一定是ReadView生成后才开启的。这里容易犯迷糊的是max_trx_id。它不是“当前最大活跃事务ID”而是“理论上的下一个事务ID”相当于一个水位线。为了记清楚你可以把事务ID想象成不断增长的门牌号ReadView生成时记录了“到目前为止最高的已分配号码”后面新进门的人门牌号都比这个边界大。生成ReadView的目的只有一个在不用加锁的情况下判断某一条记录版本对当前事务是否可见。你平时执行的普通SELECT背后的可见性判断全靠这个结构而不是靠全表扫描时逐行去问“这个事务提交了没有”。2.4 快照读与当前读一条SQL背后的两套读写逻辑MVCC里最容易被绕晕的点是快照读和当前读的区分。快照读就是普通的SELECT语句不加任何锁读取的是基于ReadView判定出的“某个历史版本”而不是物理上最新的数据。它不需要等待其他事务提交也不阻塞其他事务写入所以并发性能很好。购物网站首页查商品信息基本都是这种读。当前读则不同它要读取记录的最新版本。哪些操作属于当前读这里要记清楚UPDATE、DELETE、INSERT以及带FOR UPDATE、LOCK IN SHARE MODE的SELECT。当前读在读取前必须先加锁InnoDB通过行锁防止其他事务同时修改这条记录。因为在写之前必须读到最新值否则就可能基于一个旧值去覆盖别人的更新造成更新丢失。快照读和当前读的本质区别在于前者读的是“ReadView判定可见的历史版本”后者读的是“物理上最新的已提交数据并加锁”。理解了这个区别你才能解释一个经典现象同一事务里先普通SELECT查不到某行再执行UPDATE却能更新到那行。原因是UPDATE走当前读直接拿最新数据而普通SELECT走快照读看到的还是ReadView快照里的旧世界。面试官特别喜欢拿这个现象做追问能讲通这一点比背一百遍概念都管用。3. 把原理串起来一条SQL语句的MVCC完整执行流程3.1 从INSERT开始版本链是如何诞生的很多人以为MVCC只和查询有关其实INSERT也一样要参与版本管理新插入的一条记录最初就在版本链的链头。假设事务ID为100的事务执行一条INSERTInnoDB会为这条新记录写入聚簇索引并把DB_TRX_ID设为100DB_ROLL_PTR暂时指向空或者指向undo log中插入的回滚记录。此时版本链只有这一个节点它就是最新版本。在事务尚未提交时另一个事务如果做普通SELECT生成的ReadView里m_ids包含100这个活跃事务ID。读取这条记录时发现DB_TRX_ID100在活跃列表里说明插入者还没提交因此这条新记录对这个查询不可见。查询结果里自然查不到这条“未来数据”这与事务隔离级别要求的隔离性完全吻合。这里有个细节你可能在面试里会遇到为什么INSERT也要在undo log里留信息表面上看插入没有旧版本需要回滚到但如果这个事务回滚呢undo log里必须保留插入前的“空”状态回滚时直接把这条记录删除即可。同时在二级索引里也可能需要相应的清理操作。所以每条INSERT的undo log并非可有可无它是保障事务原子性的必要一环。3.2 UPDATE与DELETE旧版本留痕的真正意义再来看UPDATE的完整动作。事务ID为100的事务要修改一条id为1的记录该记录当前由事务ID为90的事务修改过DB_TRX_ID90DB_ROLL_PTR指向一个更老的版本。InnoDB在更新前先把当前行的数据完整写入undo log生成一个旧版本节点然后修改行数据最后更新DB_TRX_ID为100并把DB_ROLL_PTR指向刚写入的旧版本。此时版本链变成链头是事务100修改后的最新版本下一个节点是事务90修改后的版本再往下可能还有事务50、事务20留下的版本。DELETE和UPDATE的动作类似但会在记录头部加上DELETE_BIT标记。在版本链中你看到的仍然是一个“被删除前的完整版本”。于是MVCC读取时就能通过这个标记知道这行在某个时间点已经被逻辑删除但在更早的时间点它是存在的。理解这一步你就能应对一种常见场景一个事务A先查询全表事务B删掉了几行并提交事务A再次查询数据还是删掉前的样子。原因就是事务A的ReadView在第一次查询时就固定了这期间B的删除提交对A的普通SELECT不可见。只有在A执行当前读时它才会碰到已被标记删除的记录并借由锁等待逻辑去感知变化。3.3 隔离级别如何遥控ReadView的生成时机ReadView什么时候生成直接决定了整个事务里快照读“看得到什么”而这正是隔离级别存在差异的关键。先记住一个硬结论在RC读已提交隔离级别下事务里的每一次普通SELECT都会新生成一个ReadView在RR可重复读隔离级别下事务里第一次执行普通SELECT时生成ReadView之后整个事务复用同一个ReadView。为什么要这样设计因为RR的核心语义是“同一个事务里多次读取同一行数据结果必须一致”。如果每次SELECT都重新读取活跃事务列表那么其他事务在两次查询之间提交的数据就会突然“冒出来”破坏可重复读。复用同一个ReadView相当于把整个事务的可见性锁定在第一次查询的时间点。RC则不需要这种锁定。它只需要保证每次读取都能看到已经提交的最新数据因此每次生成新ReadView天然满足“读已提交”语义。面试时被问到“RR怎么解决不可重复读”你可以直接说RR通过让ReadView在整个事务期间复用使得其他事务已提交的更新在快照读路径上看不见因此同一事务内多次读取结果保持一致。真正实现这一点的不是MySQL在每条SELECT上加锁而是ReadView的生成策略。3.4 核心可见性判断五条规则背下来更要会推导ReadView生成后读取一条记录时到底怎么判断当前版本可见我把InnoDB的判断逻辑拆成五条规则这五条你在面试纸上可以直接默写出来规则一如果记录版本的DB_TRX_ID等于creator_trx_id也就是当前事务自己修改的直接可见。自己当然能看见自己改的东西。规则二如果DB_TRX_ID小于min_trx_id说明这个版本在ReadView生成之前就已经提交了因为ReadView生成时它已经不在活跃事务列表里所以可见。规则三如果DB_TRX_ID大于等于max_trx_id说明这个版本是由ReadView生成之后才开启的事务修改的绝对不可见。规则四如果DB_TRX_ID在min_trx_id和max_trx_id之间进一步判断它是否在m_ids里。在列表里说明事务还没提交不可见不在列表里说明它虽然开启得早但提交得早可见。规则五如果当前版本判断为不可见就顺着DB_ROLL_PTR找到版本链里的下一个旧版本再用同样的规则继续判断直到找到可见版本或者走到链尾。规则四是最容易出错的地方因为min和max之间的值既有可能是已提交的也有可能是未提交的。唯一的判断标准就是m_ids列表。这也是为什么ReadView必须记录活跃事务列表缺了它可见性判断就成了猜谜。背下这五条之后你的面试回答就有了一锤定音的底气。因为面试官只要不是纯刁难追问来追问去最后还是回到“某条记录对某个事务是否可见”这个判断上。4. 面试实战90秒答题框架和五个追问的应对思路4.1 我建议的答题结构先定义、再拆解、后串联面试不是论文答辩你的目标是让面试官在短时间内确认“你懂这块知识”。我建议用三段式来答总体控制在90秒到2分钟。第一段给MVCC一个精准定义。可以说MVCC是多版本并发控制它的核心思路是不去锁定一行数据而是通过保存多个历史版本来让读写并发读操作不阻塞写操作写操作也不阻塞读操作。第二段抛出实现它的三个关键机制。你的表述可以类似这样“具体到InnoDB实现依赖三个东西一是有隐藏列比如DB_TRX_ID和DB_ROLL_PTR二是有undo log版本链三是有ReadView。普通SELECT会生成或复用一个ReadView然后沿版本链判断哪个版本可见从而实现快照读。”第三段主动降维到隔离级别。强调“在RR隔离级别下事务第一次SELECT时生成ReadView并复用所以可重复读在RC下每次SELECT都重新生成ReadView所以能读到已提交的最新数据”。这个结构的价值在于它把定义、机制、影响串成一条线。面试官不仅能听到概念还能看出你脑子里有完整图景而不是零散的知识点。4.2 面试官高频追问清单每个追问背后的用意面试官在听完你的三段式后大概率会挑一个细节深挖。我整理了五个出现频率极高的问题以及它们背后的真实考察点。第一个追问RR下ReadView到底什么时候生成这是最经典的一问。标准答案是事务中第一条普通SELECT执行时才生成不是BEGIN时生成。很多人以为事务一开始就生成ReadView这是错的。如果你先执行UPDATE再执行SELECTReadView生成的时间点会更靠后可能带来一些“出乎意料”的可见性影响。第二个追问为什么RC不能解决幻读这个问题的核心在于RC下每次查询都生成新ReadView其他事务新插入并提交的数据在下一次查询时就能被看见。所以RC下可能在一个事务的两次查询里看到新增行也就是幻读。RR为了避免这种问题才让ReadView固定不变并在当前读场景配合间隙锁。第三个追问MVCC能解决写写冲突吗答案是不能。MVCC解决的是读写并发写写冲突必须靠行锁。两个事务同时改一行后面的会被锁阻塞而不是靠版本链静默处理。面试官问这个是想确认你不是真的以为MVCC万能。第四个追问为什么DELETE也会产生版本链这个问题考察undo log和purge原理。DELETE在InnoDB里也是标记删除旧版本进入版本链事务可见性判断也一样走ReadView规则。物理删除要等purge线程处理。第五个追问RR到底还可能出现幻读吗正确回答是在纯快照读场景下RR不会出现幻读但如果走当前读如SELECT FOR UPDATERR需要通过间隙锁解决幻读如果间隙锁也没覆盖依然可能出现。这个细节能体现你真的理解快照读和当前读的差异。4.3 概念混淆盘点这些坑我亲眼见过很多人踩面试和实际工作中有一批高频概念混淆我每隔一阵就能见到有人踩进去。第一个坑把MVCC当成一种锁。准确地说MVCC是用版本数据来回避锁冲突的思路它确实让读写不互斥但涉及写写冲突时还是回到行锁。和面试官聊天的时候尽量避免说“MVCC代替了锁”正确的说法是“MVCC减少了锁冲突但锁仍然是并发控制的重要部分”。第二个坑认为ReadView事务启动那一刻就生成。这个前面已经强调过事务启动和ReadView生成是两回事。尤其当你的事务先执行INSERT或UPDATE等当前读操作再执行SELECT时ReadView的生成时机可能比直觉晚很多这会影响你最终“看到多少已提交数据”。第三个坑把隐藏字段记成只有两个。DB_TRX_ID、DB_ROLL_PTR之外还有一个DB_ROW_ID虽然它只在没有主键时出现但面试官真的会问。如果你张口就漏掉会显得基础不牢。第四个坑把undo log和bin log混为一谈。undo log是InnoDB存储引擎层的用来做事务回滚和MVCC版本链bin log是MySQL Server层的逻辑日志用于主从复制和数据恢复。面试里把这两个说串了会让人怀疑你只是背面试题。第五个坑认为RR下“完全不可能幻读”。严格讲RR在快照读下不会但在当前读下还是依赖间隙锁。间隙锁没覆盖到范围时幻读仍可能发生。你要是斩钉截铁说“RR彻底解决幻读”遇到较真的面试官反而会掉分。5. MVCC的边界与进阶知道局限才叫真懂5.1 MVCC管不了写写冲突也别指望它解决一切我一直强调一个原则知道一个技术的边界要比知道它能干什么更能说明你的水平。MVCC的强项是读写并发它让“读老版本”成为可能因此读不会被写堵住。但两个写事务同时操作同一行时MVCC不会出来做调度只能交给行锁和事务调度去处理。这意味着什么在一个高并发下单场景里多个事务同时扣减库存MVCC不会让扣减操作并发执行。第二个事务要更新同一行时必须等待第一个事务的锁释放。你可以在业务层做乐观锁用版本号或者时间戳来降低冲突也可以缩短事务时间减少持锁周期。总之MVCC不是一把万能钥匙。线上排查死锁时经常遇到的场景是两个事务各自更新对方持有的行这时候分析版本链没有意义你得看锁等待关系。MVCC知识能帮你理解为什么某个事务的SELECT会看到旧版本但定位死锁还是得靠lock_wait和SHOW ENGINE INNODB STATUS。5.2 经典陷阱题快照到底在什么时候生成这道陷阱题我见过很多版本核心就一个在一个RR事务里先执行一条UPDATE再执行一条SELECT这个SELECT读到的数据是“最新已提交”还是“事务启动时的快照”我来说结论RR事务的ReadView是在第一条普通SELECT执行时才生成的。如果事务先执行UPDATE此时走的是当前读还没有生成ReadView随后执行普通SELECT才在这个时间点生成ReadView。所以如果你在事务里先更新了某一行然后再去查询整个表你会发现之前更新的行“自己当然能看见”但其他事务在UPDATE执行之后、SELECT执行之前提交的新数据对你是不可见的。这可能导致一个有趣现象这个事务的“快照”时间点不是事务开始时刻而是第一次查询时刻。很多面试者在没仔细看源码或没做过实验时都会答成“ReadView在BEGIN时就生成了”然后被面试官一步步带进“先UPDATE再SELECT”的追问里。想彻底掌握这一块建议你亲自开两个MySQL会话用事务操作配合查询验证一遍比看十篇博客都有用。5.3 从“回去等消息”到翻盘一次面试复盘的真实经验回到开头那个朋友的故事。他面完之后来找我复盘我让他把这次问答从头到尾过了一遍发现他最大的问题不是不知道MVCC而是没有一个清晰的结构去“倒出”他的知识一旦被追问就慌了回答问题变得支离破碎。我建议他下次遇到这类问题先用三段式稳住节奏定义、机制、隔离级别。至于纸面上的原理我把这篇里藏的几个关键点讲给他听隐藏列三件套、undo log版本链、ReadView四要素、可见性五规则、RR和RC的区别、快照读与当前读的区别。他花了一个周末把这些点自己动手画成一张脑图并在本地MySQL环境里做了几组对照实验验证了ReadView生成时机和快照读效果。半个月后他又面了一家同样聊到MVCC这次他不仅答出ReadView在第一条SELECT时生成还顺带提了purge线程和undo表空间膨胀的知识。面试官当场评价“这块讲得挺透的。”他后来拿到offer给我发消息说了句话让我印象很深“我终于明白面试官要的不是一个名词而是一套能把数据库并发世界讲通的能力。”我觉得这句话很适合送给每一个准备面试的后端工程师也值得你在“回去等消息”之后认认真真把MySQL并发机制补起来。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进