恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

MySQL MVCC原理详解:从隐藏列、undo log到ReadView的完整机制

  • 首页
  • 资讯中心
  • /
  • MySQL MVCC原理详解:从隐藏列、undo log到ReadView的完整机制

相关资讯

GEO优化公司比稿选型避坑笔记:从流量缺口到供应商筛选 2026/10/19 14:29:44
基于MATLAB/Simulink的4机10节点系统暂态稳定仿真与CCT扫描 2026/10/19 14:29:44
用AI工具30分钟搞定毕业答辩PPT:流程、技巧与避坑指南 2026/10/19 14:29:44

最新资讯

连续自然数求和不是简单题:OJ多组输入与输出格式全解析
MG3692B 信号源 + LabVIEW:脉冲怎么测?
从“走过场”到高质量:开放代码评审流程设计与实践
如何把Code Review变成团队工程规范?一套可落地的完整方案
基于YOLOv11的营养成分表检测与部署实战
AIGC重复率高怎么办?十大检测改写工具实测与降重全攻略

今日推荐

OpenCowork 实测:支持本地文件、飞书机器人的 Windows AI 助手(只需配置 Token)
GPT-6 Sol 与 Luna 怎么选?Chat、Work、Codex 版本差异与 TaoToken 接入实测
openclaw.net 集成 ElBruno.MempalaceNet 记忆系统:把 MCP 配置改到 TaoToken 的完整实践

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

MySQL MVCC原理详解:从隐藏列、undo log到ReadView的完整机制

发布时间:2026/10/19 14:29:44
MySQL MVCC原理详解:从隐藏列、undo log到ReadView的完整机制 面试聊到MySQL十个面试官有九个会问MVCC。八个候选人里五个能像背课文一样说出“MVCC就是多版本并发控制通过隐藏列、undo log、ReadView实现”但再往下追问一句“RC和RR的ReadView到底哪里不一样”或者“版本链是怎么一步步长出来的”基本就卡壳了。八股文背得再熟不理解底层机制线上出问题照样抓瞎。我自己做过很长一段时间后端和数据相关的工作说句实在话MVCC并不算多难难点在于它把好几个机制串在了一起隐藏列、undo log、ReadView、隔离级别、当前读和快照读哪个环节没串起来理解就会断层。这篇文章我就把这些环节一个个拆开结合一条数据从插入到修改的全过程把MVCC的真实工作方式讲透。不管你是正在准备面试还是日常排查数据库问题看完都应该能有一个比较完整的框架。1. 先从并发场景说起为什么数据库非要搞出“多版本”1.1 读写冲突是并发系统绕不开的核心矛盾任何一套业务系统只要用户量上来并发读写就是躲不掉的。拿一个最简单的电商订单表来说用户下单要insert后台改状态要update用户查订单要select。多个请求同时落在同一张表、甚至同一行数据上数据库必须保证数据不出乱子。最朴素的做法就是加锁。读之前加共享锁写之前加排他锁读写互相阻塞。这样做数据绝对安全代价就是并发能力直线下降。一个写操作长时间占用锁所有读操作都得排队等着用户那边表现就是页面卡顿、接口超时。一个读操作占用共享锁写操作也得等着系统吞吐量上不去。在互联网高并发场景下这种“一刀切”的加锁方案根本撑不住。所以InnoDB引入了MVCC全称Multi-Version Concurrency Control多版本并发控制。核心思想一句话就能说清一条数据在数据库里并不是只有一个“最新值”而是同时存在多个历史版本。读操作去读符合条件的“旧版本”写操作去改“最新版本”读和写各自干各自的互不堵路。1.2 把“一把大锁”变成“多条车道”用生活里的场景来类比假设你和同事在协作编辑一份在线文档A同事正在修改某个章节如果此时其他同事都不能看这个章节那协作效率会非常低。MVCC的思路相当于A同事修改的同时其他人看到的是这个章节上一秒保存的版本等A改完提交大家再看到最新版本。修改的瞬间不阻塞阅读阅读的瞬间也不阻塞修改。放到数据库里MVCC最大的价值就是实现了“读写不互斥”。读操作不需要等待写操作释放锁写操作也不需要等待读操作完成理想情况下甚至可以做到读写完全并行。同时每条数据都保留了历史版本事务在回滚时也能找到旧数据可以说是一举两得。但注意MVCC并不是完全抛弃锁。插入、更新、删除这类写操作在真正执行时依然要对记录加锁防止两个事务同时改一条数据。MVCC主要优化的是普通SELECT快照读和写操作之间的冲突这也是理解MVCC边界很重要的一点。我们后面专门展开说。2. 管中窥豹MVCC需要哪三样“地基”2.1 隐藏列每条记录的“身份证”和工作履历先看一个基本事实InnoDB里一张表除了你定义的业务字段每条记录还会额外携带几个隐藏字段。它们平时不显眼但MVCC的整个逻辑全压在它们身上。相关的隐藏列主要有三个DB_TRX_ID最近一次修改这条记录的事务ID。事务ID由系统分配一般来说越晚开启的事务ID越大。DB_ROLL_PTR回滚指针指向undo log里这条记录上一个版本的位置作用是把各个版本串成一条链。DB_ROW_ID如果表没有定义主键InnoDB会隐式生成一个自增的行ID用于聚簇索引组织。有主键的表不会用到它。可以把这三个字段理解成一组“工作履历”DB_TRX_ID写着“这个版本是谁改的”DB_ROLL_PTR写着“上一个版本存在哪”。版本链在物理上靠DB_ROLL_PTR串起来在逻辑上靠DB_TRX_ID判断哪个版本对当前事务可见。2.2 undo log 版本链回滚的目的不只是回滚每次执行UPDATE、DELETE、INSERT时InnoDB都会把修改前的数据记录到undo log。很多人对undo log的理解停留在“事务回滚要用”其实它还有另一层关键使命就是支撑MVCC。一条记录被反复修改多次每个旧版本都会以undo log的形式存在。新版本记录里的DB_ROLL_PTR指向最近一次undo log的位置undo log里又继续指向上一次修改前的undo log这样一层层往回指就形成了版本链。举个例子。一条记录初始值是A事务100改成B事务200改成C。此时记录的最新值是C它内部通过回滚指针能指到“值为B”的undo log再通过B版本的undo log继续指到“值为A”的原始版本。链头是最新值链尾是最早的初始版本。所以当一个事务读到某个版本后发现这个版本对自己不可见就会顺着DB_ROLL_PTR往下找直到找到一个可见的版本为止。整个过程不需要去别的地方做复杂关联回滚指针天然就是一条现成的查询路径。2.3 ReadView某个时间点的“全局快照相机”有了版本链之后理论上就能读取历史版本了。但数据库需要回答一个更关键的问题对于一个正在执行的事务而言版本链上的哪个版本“合法”这就是ReadView要解决的问题。ReadView可以理解成在“生成读视图”这一刻对数据库所有正在活跃的事务拍了一张快照。它主要记录了这么几个信息m_ids生成ReadView时系统中所有活跃未提交事务的ID列表。min_trx_idm_ids里最小的事务ID。max_trx_id生成ReadView时系统分配过的最大事务ID加1也可以理解为下一个将要分配的事务ID。creator_trx_id生成这个ReadView的事务自己的ID。判断某个版本的DB_TRX_ID是否可见本质就是用这个版本的生成者身份去对比上面几个边界值。我常说ReadView不是锁它不阻止任何人提交或回滚它只是在生成的那一刻把“哪些事务当时还没提交”这件事固定下来给后续的可见性判断一个依据。2.4 可见性判断规则拿着版本号比对四条边界很多人背了ReadView的定义但不会用。真正落到版本可见性判断时就是一套固定规则拿版本的DB_TRX_ID和ReadView的四个信息做比较。如果版本的DB_TRX_ID等于creator_trx_id表示这个版本是当前事务自己改的自己当然能看自己改的数据可见。如果版本的DB_TRX_ID小于min_trx_id表示修改这个版本的事务在当前ReadView生成之前就已经提交了可见。如果版本的DB_TRX_ID大于等于max_trx_id表示修改这个版本的事务是当前ReadView生成之后才开启的对当前事务不可见。如果DB_TRX_ID在min_trx_id和max_trx_id之间需要再检查它是否存在于m_ids活跃列表里。如果不在说明事务已经提交可见如果还在m_ids里说明事务尚未提交不可见。这四条规则就是MVCC里“过滤器”的全部内容。当前事务拿版本链上最新版本的DB_TRX_ID来比对如果不可见就沿着DB_ROLL_PTR找到上一个版本再套用同样规则继续比对直到找到可见版本或者走到链尾。3. 核心机制拆解一个事务“看见”数据的完整过程3.1 快照读和当前读两种完全不同的读取方式在真正走流程之前必须先分清两种读取方式。因为MVCC不是对所有SELECT都生效的只有快照读才会走ReadView逻辑。快照读Snapshot Read就是不加锁的普通SELECT语句。它读取的是一条数据的某个历史版本不需要等待其他事务释放锁。快照读的执行依赖MVCC的可见性判断是MVCC的主要受益者。当前读Current Read读取的是记录的最新版本并且要对读取的记录加锁防止其他事务同时修改。最常见的当前读包括SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE以及INSERT、UPDATE、DELETE操作。这些操作必须读到最新值因为你要基于最新值做修改逻辑读到一个旧版本去更新数据就会出大问题。MVCC解决的是“快照读和写操作不互斥”的问题而当前读之间、当前读和写操作之间依然通过锁机制来保证安全。这也是为什么RR隔离级别下MVCC能避免很多幻读场景但无法完全杜绝当前读造成的幻读。这个点后面单独展开。3.2 一次普通查询如何顺着版本链找可见版本现在把整个流程串起来走一遍。假设有一个账户表id1的balance初始值是100。下面有两个事务并发操作事务Atrx_id100开启执行普通SELECT查询balance。 事务Btrx_id200开启执行UPDATE把balance改成200但还没有提交。表里实际的数据是balance200最新版本由事务B修改它的DB_ROLL_PTR指向balance100的undo log版本。事务A执行SELECT时会生成一个ReadView。此时m_ids[100, 200]min_trx_id100max_trx_id201。事务A先读取到最新记录balance200拿到它的DB_TRX_ID200。200在m_ids里说明事务B还没提交这个版本对事务A不可见。于是事务A沿着DB_ROLL_PTR找到balance100这个版本的DB_TRX_ID是某个小于min_trx_id的事务也就是说修改它的老事务早就在快照生成前提交了可见。最终事务A查到balance100。如果事务B在A的SELECT之后提交了A再执行一次普通SELECT由于ReadView复用的原因A看到的依然是balance100。这就是可重复读的典型表现同一事务多次读结果一致。3.3 用伪代码说明判断逻辑把上面的规则翻译成伪代码会更清晰function isVisible(version, readView): trxId version.trx_id if trxId readView.creator_trx_id: return true if trxId readView.min_trx_id: return true if trxId readView.max_trx_id: return false if trxId not in readView.m_ids: return true return false function getVisibleRecord(record, readView): current record while current is not null: if isVisible(current, readView): return current current current.roll_pointer return null这段逻辑不只是面试用来背的线上排查慢查询或者诡异数据时理解了它你才能快速定位是隔离级别配置问题还是长事务导致undo log堆积问题。4. 从一条UPDATE看版本链如何“长出来”4.1 更新操作做了三件隐秘的事很多人以为UPDATE就是简单地把值改掉实际InnoDB内部做了更多事。一条UPDATE的执行大致分为三件事对目标记录加排他锁确保没有其他事务同时修改。把更新前的旧值写入undo log形成旧版本。更新记录的新值并把记录的DB_TRX_ID改成当前事务IDDB_ROLL_PTR指向刚才写入的undo log位置。加锁保证并发安全写undo log保留历史版本改隐藏列让版本链延伸。这三件事缺一不可。这里有个容易被忽略的细节如果UPDATE修改了聚簇索引的键值可能导致记录移动位置此时undo log里还会记录主键的旧值用于回滚时还原。如果只是普通字段更新undo log主要记录被更新字段的旧值即可。InnoDB会根据实际修改情况决定undo log里要保存哪些信息。4.2 模拟两个事务并发修改同一行数据用一个完整的例子模拟两个事务同时修改同一条记录看看版本链的变化。初始情况id1记录balance100最后修改它的事务ID50。时间线如下事务Atrx_id100执行UPDATE把balance从100改成150。此时最新记录balance150DB_TRX_ID100DB_ROLL_PTR指向balance100的undo log。版本链150 → 100。事务A未提交事务Btrx_id200也执行UPDATE。注意事务B要更新这行必须先等事务A的排他锁释放。事务A提交后事务B才能继续。事务B把balance从150改成200。执行前先把balance150写入undo log然后更新最新值为200DB_TRX_ID200DB_ROLL_PTR指向balance150。版本链200 → 150 → 100。此时有一个事务Ctrx_id300执行普通SELECT生成ReadView时事务B还在活跃列表里那么事务C会顺着版本链找到balance150吗不会它会继续判断150这个版本的DB_TRX_ID100如果事务A已经提交且100小于min_trx_id那就可见查到150。这个例子能说明两个重点每执行一次提交后的UPDATE版本链就多一层普通查询不是只找第一层而是一层一层去匹配直到找到合法的版本。4.3 版本链不可能无限增长purge线程在背后兜底版本链如果一直长大undo log会膨胀占用大量磁盘空间查询也会因为需要遍历太长的版本链而变慢。InnoDB专门有一个后台purge线程负责清理过期的undo log。purge的清理原则是当某个旧版本已经没有任何活跃事务的ReadView需要引用它时它就可以被标记删除并回收空间。换句话说版本链上的旧版本不是立即删除而是等“没人再需要”之后再清理。这个“没人需要”的判断依赖系统对活跃事务和ReadView全局信息的维护。我见过不少线上事故就是因为某个长事务长期不提交导致ReadView一直保存着很早的快照信息purge线程不敢清理undo log磁盘空间持续增长。所以监控长事务不只是业务层面的问题它会直接影响InnoDB内部的存储和性能。5. 隔离级别下的MVCC差异RC和RR到底差在哪5.1 RR级别下ReadView只在第一次快照读时生成MySQL默认的隔离级别是REPEATABLE READ可重复读。在这个级别下一个事务内部如果执行多次普通SELECT只在第一次SELECT时生成ReadView后续所有快照读都复用同一个ReadView。复用ReadView会带来什么效果事务一开始就确定了“能看到的版本边界”之后再怎么并发修改这个事务看到的数据都是一致的不会出现两次查询结果不一样的情况。这正是“可重复读”这个名称的含义。用前面的例子事务A先查询balance100随后事务B修改并提交事务A再次查询balance因为它复用了第一次的ReadView事务B的修改版本依然不可见结果还是100。5.2 RC级别下每次快照读都重新生成ReadViewREAD COMMITTED读已提交级别下每次执行快照读都会生成一个新的ReadView。这意味着事务内两次普通的SELECT可能读到不同的结果。继续上面的例子事务A第一次查询balance100生成ReadView1看不到事务B的修改。事务B提交后事务A第二次查询会重新生成ReadView2。此时事务B已经不在活跃事务列表里它对balance的修改版本对当前事务可见于是第二次查询结果变成了200。两次查询结果不一样这就是“读已提交”的行为特征每条语句都能读到最新已提交数据。所以面试如果问你RC和RR在MVCC上的区别本质上就是一句话RC每次快照读都生成新ReadViewRR复用事务内第一个ReadView。理解了这句再结合实际读写场景展开就比单纯背定义有说服力得多。5.3 为什么RR下还会出现幻读先说幻读是什么同一个事务内执行两次同样的范围查询第二次多出了一些第一次没看到的行。很多人会问RR隔离级别下MVCC不是已经保证快照读一致了吗怎么还会幻读关键在于MVCC的快照读只能保证“快照读”之间的一致不能保证“当前读”之间的一致性。如果一个事务先做了一次快照读另一个事务插入了新记录并提交之后该事务再执行带锁的当前读比如SELECT ... FOR UPDATE这时当前读会读取最新已提交的数据就可能把另一事务刚插入的记录也查出来产生幻读。另外还有一种情况如果事务在快照读之后执行了UPDATE或INSERT这类当前读之后再进行快照读由于当前读会读取最新版本有可能会间接影响之后看到的版本造成“自己的修改”“自己新插入的数据”和最初快照不一致的复杂情况。严格来说靠MVCC无法彻底天然解决幻读InnoDB在RR级别下还引入了间隙锁gap lock和临键锁next-key lock用来锁定范围防止其他事务在区间内插入记录。MVCC解决快照读的幻读感知间隙锁解决当前读的幻读问题两者配合才把RR级别拉到了接近序列化的强度。5.4 两类隔离级别下的MVCC行为对比用一张表把差异整理清楚对比维度READ COMMITTEDREPEATABLE READReadView生成时机每次普通SELECT都生成新ReadView事务内第一次普通SELECT生成后续复用同事务多次快照读结果可能不同读到最新已提交数据一致基于同一个快照版本链遍历深度相对较浅能看到最新提交版本可能较深需要回溯到快照时刻的可见版本潜在问题不可重复读快照读无幻读当前读仍需锁机制辅助如果业务对数据一致性要求高使用RR更省心但要注意间隙锁可能带来更大概率锁等待。如果业务可以接受一条语句内看到最新已提交数据RC对并发更友好很多互联网大厂在高并发场景下反而更倾向RC。6. 面试高频追问这样回答才算真正懂得MVCC6.1 追问一RR级别下普通SELECT能看到另一个事务没提交的修改吗看不到。普通SELECT走快照读会生成ReadView所有未提交事务的修改版本都在m_ids活跃列表里被判定为不可见。这是MVCC的核心逻辑也是“读已提交”“可重复读”两种隔离级别都具备的能力不会读到脏数据。6.2 追问二UPDATE为什么不能走快照读必须当前读因为UPDATE要修改数据它必须先拿到最新版本的值再做变更。如果基于旧版本做修改就会覆盖其他事务刚提交的新值产生丢失更新。所以UPDATE会对记录加排他锁读取最新版本执行修改。MVCC的快照读只用于查询场景写入场景必须走当前读。说到丢失更新还有一类容易被问到的场景两个事务都先SELECT基于同一个旧值做计算再UPDATE最终只有一个更新生效或者晚提交的覆盖早提交的。MVCC本身不能完全防止这类问题需要额外的锁机制或版本号方案配合。6.3 追问三DELETE操作和MVCC有什么关系DELETE也是写操作执行时会对记录加排他锁同时把当前版本写入undo log形成旧版本再在记录上打一个删除标记。从MVCC角度看被删除的记录并没有物理删除只是标记为删除历史版本依然在版本链上。普通SELECT判断可见性时如果当前标记为删除的版本不可见会继续找上一个历史版本。这就是为什么一个事务已经提交删除记录另一个较早开启的事务还能读到它。6.4 追问四长时间不提交的事务对MVCC有什么影响影响主要有两个。第一它持有的ReadView会一直引用某些旧版本导致purge线程无法清理对应undo log磁盘占用只增不减。第二版本链越来越长后续查询遍历成本增加整体性能下降。线上遇到undo log膨胀时第一时间要查是否有长时间未提交的事务。排查方式通常是查information_schema下的事务相关状态表找到运行时间较久的活跃事务提醒相关业务方尽快提交或回滚。如果事务确实要长时间运行可以考虑拆分事务、降低单个事务的运行时长从源头减少对MVCC版本链的压力。6.5 追问五如果用一张表把MVCC的工作要素列全你会列什么这个问题是很多面试官爱问的开放题。我一般按下面分类来答存储结构隐藏列DB_TRX_ID、DB_ROLL_PTR和undo log组成了版本链。判断依据ReadView记录了活跃事务ID列表和边界信息。可见性规则用版本事务ID和ReadView的边界值做比较决定版本是否可见。读取方式快照读普通SELECT走MVCC当前读加锁SELECT和写操作走最新版本加锁。隔离级别差异RC每次快照读新生成ReadViewRR复用事务内第一个ReadView。这套框架把存储、判断、读取、场景差异全部覆盖到了面试官想往里深入每个点你都有话可接。结尾MVCC本身不是一个能独立工作的组件它更像一套“机制组合”底层靠隐藏列搭起版本链结构执行时靠ReadView提供判断依据配合undo log保留历史版本最终让读写并发场景下查询不阻塞、写入不阻塞。我个人在实际排查问题中最深的体会是很多人对MVCC的理解停留在“历史版本”这四个字上一旦线上出现undo log膨胀、RR级别下查到“不该看到”的数据、或者一条慢SQL在版本链上遍历太长就不知道从何查起。其实这些问题几乎都能回到同一个思路当前事务的ReadView到底覆盖了哪些事务版本链的哪个版本对这个ReadView可见。把这个核心问题想清楚MVCC的基本功就算真正过关了。最后分享一个小技巧准备面试或者写技术笔记时不要只背结论最好自己动手画一条版本链标上事务ID和回滚指针再模拟几次可见性判断。画过一次之后你会发现再遇到相关的问题脑子里会自动浮现这条链而不是去死记规则。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号