设想你走进一家传媒大楼:一楼大厅里有一面巨大的公告墙,墙上贴满了各部门的通知。每天都有职员走近这面墙——有人掏出相机拍一张照片,按照片内容办事;有人则直接走到墙前,用笔改动墙上的文字。最妙的是:拍照的人从来不会被改墙的人打扰,改墙的人也从来不用等拍照的人离开。这就是MVCC(多版本并发控制)的日常图景。 在传统数据库里,一旦有人正在修改一条数据,其他人读这条数据就得排队等待,就像公告墙前只能有一个人写字、其他人只能干瞪眼。MVCC改变了这一切:它让数据库里的每一行记录都像公告墙上的通知一样,贴着一张张的历史照片(旧版本)。你想看数据?拍张快照就行,哪怕此刻正有人挥笔涂改,你手里的照片依然清晰完整。你想改数据?也不用担心读者排队围观,你只管改你的,改完把新版本挂上墙,旧照片留在档案室做底片。 这篇文章将带你拆解MVCC的完整机制:多版本如何存储、事务之间如何判断“谁的照片对我可见”、快照读与当前读有什么区别、版本链如何回溯清理、以及工程上要避开的坑。你会发现,数据库并发控制并没有想象中那么神秘——它不过是一套“拍照、贴新版、存底片”的精细流程。
对AI从业者而言,理解MVCC是读懂数据库隔离级别、排查慢查询与锁等待、设计高并发数据链路的基础能力,也是面试中区分“调包侠”与“懂原理者”的经典试金石。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
MVCC核心概念
回到传媒大楼。在没有MVCC的年代,公告墙前立着一块“请勿靠近”的警示牌:谁要改墙上的通知,所有人必须退到三米外,等他把字写完才能凑上去看。更夸张的是,连“想看一眼”的人多了,也会互相挤得不可开交——这就是经典的读写互斥、读读也互斥。后来大楼引入了新规矩:公告墙永远只有一个,但每个职员手里都有一只“时光相机”。
所谓“多版本”,指的是同一行数据在数据库中同时保留多个历史修订版本,就像同一张公告的初稿、二稿、终稿都被照片留存。每个版本都对应一次事务修改,版本之间用链条串起。当一个事务发起读取时,它不需要阻塞别人,只需要声明“我按哪一刻的照片来读”——其他事务照常改墙,互不干扰。
MySQL的InnoDB引擎正是这样做的。它把每行数据的多个版本存放在表空间中,通过隐藏列和undo log维护版本链。读操作走快照读时,根据ReadView判断该读哪个版本;写操作则生成新版本并挂到链条头部。于是“读写并发”从梦想变为现实:读者看历史照片,写着贴新公告,各干各的。
所以,MVCC的核心贡献不是“解决一切冲突”,而是把不可并行的读写操作拆成了可以并行的快照世界。职员们再也不用站在公告墙前干等——每个人手里那张照片,就是属于他自己的一片时空。
- 并发冲突率指的是在大量并发事务中,真正发生“两人抢同一张公告改”的比例。
- 分母是全部操作包含所有读写操作,MVCC把读操作从互斥队列中解放出来,分母越大、冲突率越低。
- 分子是写写碰撞只有两个写事务瞄准同一行时才需要锁,分子越小,系统越流畅。
隐藏列与版本链
每张公告被拍照存档时,大楼管理员都会在照片背面写两行字:一行是“最后修改人编号”,一行是“上一张底片的存放柜号”。前者告诉后人这版是谁定的稿,后者让档案员能顺着线索翻出更早的底片。这两行字,在InnoDB中就是隐藏列。
InnoDB为每行数据额外维护了两个关键隐藏列:trx_id(最近修改该行的事务编号)和roll_pointer(指向 undo log 中上一个版本的指针)。当一条记录被修改时,修改前的内容会被写入undo log,roll_pointer 指向这条 undo 记录,而 trx_id 记录当前修改者。多个版本的记录由此串成一条从新到旧的版本链,链头是当前最新值,链尾是最初的插入值。
这条链让数据库在“读旧版本”时有了精确的路径:给定一个快照读请求,沿指针一路向前,直到找到对当前事务可见的版本。它相当于给每张公告配了一条“时光回溯线”,档案员(数据库)只需沿着底片柜号一路翻找,就能定位任意历史时刻的公告内容。
回到公告墙的比喻:照片背面的“底片柜号”就是 roll_pointer,“最后修改人编号”就是 trx_id。档案室里的底片串成一条完整的修改史,而数据库的每一次查询,就是档案员沿着这条线做一次“时光倒流”。
- 修改次数累加每次
UPDATE或DELETE都会产生一个 undo 版本节点,不会原地覆盖。 - 加一是初始记录最开始的
INSERT形成了一个不可再往前的版本,它是链条的地基。 - 链条方向从新到旧
roll_pointer总是指向“更早”的版本,读操作顺着指针前行。
事务隔离级别
传媒大楼对“拍照”和“看墙”的规矩分了四个等级,从宽松到严格,对应着数据库的四种隔离级别。最宽松的级别是读未提交(RU):职员不管照片上签名是谁、是否已经定稿,看到墙上有什么就读什么——这相当于连照片都不拍,直接肉眼看现场,别人没写完的草稿也看。然后是读已提交(RC):每次拍照都以按下快门的瞬间为准,别人一旦改完贴上墙,下一次拍照就会读到新内容——照片每次都是新的。
再严格一些是可重复读(RR):事务第一次拍下的照片,整个事务期间都用这一张,无论别人改了多少次墙,你看到的世界始终是开拍那一刻的模样。最严格的串行化(Serializable)则完全禁止并发,所有人排队看墙、排队改墙。InnoDB的MVCC在RU、RC、RR三个级别上分别调整“拍照”的规则,让隔离级别成为快照生成策略的调节旋钮。
核心差异在于ReadView的生成时机:RC每个语句都生成新的ReadView(每次拍照都是新照片);RR只在事务第一次读时生成ReadView(一张照片用到底);RU根本不生成ReadView(直接看现场)。这种设计让隔离性不依赖锁,而是依赖“照片的新旧程度”,赋予了数据库极大的灵活性。
就像不同楼层有不同的管理条例:RC是“进门前拍一张”,RR是“上班时拍一张管一天”。MVCC之所以能支撑不同隔离级别,正是因为它把“可见性规则”做成了一块可插拔的取景卡,想严格就换个卡,不必推翻整面墙。
- RR的首次决定事务内第一条 SELECT 语句执行时生成 ReadView,之后所有快照读都复用。
- RC的每次刷新读已提交在每个 SELECT 语句开始时都重新生成 ReadView。
- 可见性绑定生命周期RR 下旧照片防止“不可重复读”,RC 下新照片允许读到已提交的新数据。
ReadView机制
现在最核心的问题来了:照片照下了满墙公告,可墙上的每张公告都签着不同的“最后修改人编号”。你怎么知道哪些编号的修改你已经知道了、哪些人的改动你还不该看?大楼给每台相机配了一张取景许可卡(ReadView)。这张卡上记录着:按下快门那一刻,还有哪些同事正在办公室里改稿(活跃事务列表),以及他们的编号范围。
ReadView 的核心结构包括三个重要字段:当前未提交事务的 ID 集合 m_ids(活跃事务数组)、活跃事务中最小的 ID min_trx_id、下一个待分配的事务 ID max_trx_id。判断一个版本是否可见时,数据库按照一套固定规则查卡:版本事务ID小于min_trx_id,说明这个修改发生在拍照之前、已经尘埃落定,可见;大于等于max_trx_id,说明是拍照之后才启动的事务,不可见;介于两者之间,则查它在不在活跃数组里——不在,说明已提交,可见;在,说明还没结束,不可见。
这套“三段式判断”是MVCC可见性规则的核心,也是面试中最常考的细节。它本质上是一张时间轴上的定位表:把每个事务ID看作时间戳,ReadView就是“我此刻承认哪些时间点已发生过”。数据库遍历版本链时,对每个版本执行一次这个判断,直到找到第一个可见版本为止。
取景许可卡就是ReadView的通俗化表达。每台相机按下快门时,卡上已经印好了“此刻谁是活人、谁是历史”的名单。数据库沿版本链每遇到一个版本,就掏出卡对照一次——就像档案员逐张翻看照片背面的签名,比对许可卡上的人名,终于找到那张“该看”的照片,递给读者。
- 早期修改必可见事务ID小于活跃下限,说明修改发生在快照生成前且已提交,直接通过。
- 新事务不可见事务ID不小于下一个分配ID,说明它是快照之后才出生的,对当前事务来说是“未来人”,不可见。
- 中间地带查活跃表ID在两者之间的,看它是否还在
m_ids活跃列表中;不在列表说明已提交,可见。
快照读与当前读
在公告墙前有两种截然不同的“看”法。第一种,职员举起相机拍张照片,然后拿着照片回工位慢慢看——这叫快照读(Snapshot Read),在SQL里就是普通的 SELECT 语句。拍照片不碰墙,不需要锁,永远不阻塞别人。第二种,职员必须直接站到墙前,用手摸着最新公告确认现状——这叫当前读(Current Read),对应 SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE 等操作。既然要动手改墙,就必须给墙上的这块区域挂上“使用中”的牌子(加锁),防止别人同时改。
快照读依赖版本链和ReadView,不加任何锁,因此并发度高;但它读到的数据可能是历史版本。当前读读的一定是最新数据,因为修改操作必须基于最新状态做更新,否则更新会丢失。在InnoDB中,当前读默认加行级排他锁,直到事务提交或回滚才释放。
在当前读的场景下,MVCC的“多版本”优势发挥不出来——它必须锁住最新版本,防止两个事务同时更新同一行。这正是MVCC的边界:读-读不冲突、读-写不冲突,但写-写必须串行。
用传媒大楼的话说:快照读是“拍张照片回工位看”,当前读是“亲临现场动手改”。MVCC提供了照片这条便捷通道,但真正要动墙上的字时,人们还是得在墙前排队——这就是为什么数据库里写操作永远比读操作需要更多敬畏。
- 最新版本锁定当前读直接取版本链链头,不加回溯。
- 排他锁保护写锁定期间其他写事务必须等待,直到锁释放。
- 快照读无需锁普通SELECT走ReadView取可见版本,天然无锁。
快照读:SELECT * FROM account WHERE id = 1; -- 无锁,读可见版本当前读:SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 加排他锁,读最新版
版本链查询与清理
时间长了,档案室里的底片越堆越多。每当一个快照读请求进来,档案员就要沿着底片链条往回翻——从链头开始,逐张查看照片背面的签名,对照ReadView判断是否可见。这个回溯过程在版本链很短时几乎无感,但如果一条公告被改了几十次,档案员就得翻几十张底片才能找到目标版本,这就是版本链查询的性能成本。
数据库并非永远保留所有版本。InnoDB有一个后台线程 purge(清理线程),它的职责类似于档案室的定期盘点:当一个旧版本不再被任何活跃事务的ReadView引用时,就可以把它从链上摘除并释放空间。清理逻辑遵循“版本覆盖策略”——新版本的写入不直接覆盖旧版本,而是通过 roll_pointer 把旧版本挂起来,等待purge线程确认安全后统一回收。
这里有个典型的坑:长事务会让旧的ReadView一直活着。想象某个职员第一天拍了一张照片,然后休假一个月——这一个月里档案室不敢丢弃任何底片,因为他的照片可能需要回溯到任意一个旧版本。于是undo log不断膨胀,磁盘空间暴涨,版本链回溯越来越慢。
档案室的盘点员(purge线程)每天都在工作,但只要有一个人攥着旧照片不放(长事务),他就得小心翼翼保留所有底片。理解了这一点,你就明白为什么MySQL DBA总告诫“别开长事务”——那不是洁癖,而是为了不让档案室被底片淹没。
- 总版本数版本链上所有历史版本的数量,等于修改次数加一。
- 最大保护量所有未结束事务中,最早生成的ReadView能回溯到的版本位置,决定了回收的安全边界。
- 差值即垃圾保护范围之外的多余版本可以被purge线程安全回收。
性能特性分析
从传媒大楼的经验看,MVCC带来的最大性能收益是读写并行度的提升。在没有MVCC的锁机制下,读写互为仇敌:读者挡写者、写者挡读者,系统的吞吐量被锁等待掐住咽喉。MVCC让读操作彻底脱离锁的束缚——写真写、读真读,两者各走各的通道。在典型的“读多写少”业务场景(如商品详情页、报表查询)中,并发吞吐量可以提升一个数量级。
任何收益都有代价。MVCC的代价首先是存储开销:每一行都携带版本信息,每次更新都在undo log中写入完整旧值,磁盘占用天然膨胀。其次是版本回溯延迟:快照读在版本链较长时需要多次回溯,CPU与IO成本上升。最后是清理压力:高更新频率加上长事务,会让purge线程疲于奔命,甚至导致undo空间爆炸。
做性能规划时,需要理解MVCC的“甜蜜区”:读多写少、事务短小、更新集中于热行。当写入比例升高时,版本链快速变长,MVCC的优势让位于版本维护的成本,此时需要考虑分库分表或减少热点行更新。
| 因素 | 对MVCC性能的影响 |
|------|------------------|
| 读多写少 | 正向:读走快照无锁,收益大 |
| 长事务 | 负向:阻止purge回收,undo膨胀 |
| 热点行频繁更新 | 负向:版本链变长,回溯变慢 |
MVCC就像给大楼装了一套“照片直通车”系统:想看的人走照片通道,想改的人走现场通道,两条路互不干扰。唯独通道的维护成本(档案室空间、回溯耗时)始终存在,管理者需要在“并发便利”与“维护成本”之间找到平衡点——这就是数据库调优的核心命题。
- 并发请求量系统同时处理的读写事务数,是吞吐的分子。
- 锁等待时间写-写冲突造成的排队阻塞,MVCC将其最小化但不能归零。
- 版本回溯开销每次快照读沿版本链寻找可见版本的时间。
工程实践要点
理解了MVCC的原理,最终要落回工程实践。第一条铁律:坚决避免长事务。长事务如同攥着旧照片不撒手的人,它会让ReadView长期存活,purge线程不敢清理任何旧版本,undo log无限膨胀。实践中,应该在应用层为事务设定超时阈值,把大事务拆成多个小事务,并确保在事务中只做必要的数据库操作,不在事务里调用远程接口或执行耗时计算。
第二条:合理设计索引,避免更新放大。InnoDB的MVCC对二级索引的处理与主键索引不同——二级索引的更新需要额外的回表操作来维护版本信息。如果一张表有过多二级索引,每次更新都要同步修改索引,版本链维护成本倍增。实践中应精简索引数量,避免对长字符串列建索引,尽量使用自增主键减少页分裂。
第三条:监控undo与版本链状态。通过 information_schema.innodb_trx 查看当前活跃事务,通过 SHOW ENGINE INNODB STATUS 观察undo log的增长趋势。如果发现undo空间持续高水位,优先排查是否存在长时间未提交的事务,再考虑调整 innodb_purge_threads 等清理参数。这条监控习惯,能让很多隐蔽的性能问题在爆发前浮出水面。
| 实践建议 | 具体操作 |
|----------|----------|
| 短事务 | 设置事务超时、拆分批量操作 |
| 精简索引 | 删除冗余二级索引 |
| 监控 | 周期性检查活跃事务与undo增长 |
回到大楼的管理智慧:一个合格的行政主管会规定“档案借阅时限”(事务超时),会定期盘点底片柜(purge监控),会劝导职员不要一次借太多底片(避免长事务)。MVCC的工程实践,本质上就是把这套管理经验翻译成数据库配置项和编码规范。
- 持续时长事务从开启到提交的时间,以秒计。
- 更新频率该事务每秒执行的写操作次数。
- 积压乘积关系时长与频率相乘,得到该事务造成的undo积压量。
AI 应用场景
电商高并发秒杀系统的库存扣减
阶段:数据访问层设计
解决的问题:秒杀场景下,成千上万的用户同时查询和抢购同一件商品。传统加锁方案会导致大量读请求排队阻塞,系统吞吐骤降;MVCC则让商品信息的查询走快照读,仅扣减库存的写操作使用当前读加锁,把“看”的流量和“买”的流量分开,极大缓解了锁竞争。
具体案例:某电商平台在618大促中,商品详情页的QPS冲到50万,库存扣减接口的TPS约5万。通过MVCC的快照读支撑详情页高频刷新,配合当前读加行锁保证扣减准确,数据库CPU负载维持在合理水位,未出现大面积锁等待超时。
金融报表系统的长事务隔离
阶段:数据分析与报表生成
解决的问题:银行月底生成对账报表时,需要读取某一时间点的全量数据,并要求整个报表生成过程中数据保持一致(可重复读)。如果每读一行都加锁,整个报表跑数期间业务将被冻结;MVCC的可重复读隔离级别让报表查询在事务开始瞬间生成ReadView,全程读取同一快照,不阻塞任何在线交易写入。
具体案例:某银行夜间批量跑批生成当日交易汇总报表,运行时长约40分钟,期间核心账务系统仍在正常受理存取款。正是依靠MVCC的RR隔离级别,报表查询与在线交易互不干扰,账务数据最终保持一致。
内容平台的草稿编辑与发布
阶段:应用层版本管理
解决的问题:在线文档编辑器中,用户打开文章后可能长时间停留,期间其他协作者也在编辑。若采用简单锁机制,打开文档就会锁住整篇文章,其他编辑全被阻塞。MVCC让编辑者的读取走快照,保存时用当前读比较版本号,发现版本冲突则提示合并,实现了不阻塞的并发编辑体验。
具体案例:某知识社区的文章编辑器,作者A和作者B同时编辑同一篇文章。A先保存,B后保存时系统检测到版本链变化,提示“内容已被他人更新”,B可选择基于最新版本重编或强制覆盖,避免了静默丢失更新。
Python 演示
class MVCCRow: """模拟数据库中的一行记录,内部维护版本链""" def __init__(self, init_value): self.version_chain = [(0, init_value)] # (事务ID, 值) def update(self, trx_id, new_value): """写操作:挂一个新版本到链头""" self.version_chain.append((trx_id, new_value)) def read_with(self, view): """按ReadView沿版本链回溯,返回可见值""" for trx_id, value in reversed(self.version_chain): if view.is_visible(trx_id): return value return None class ReadView: """模拟ReadView:用活跃事务集合判断可见性""" def __init__(self, active_trx_ids): self.active = set(active_trx_ids) self.min_id = min(active_trx_ids) if active_trx_ids else 0 self.next_id = max(active_trx_ids) + 1 def is_visible(self, trx_id): if trx_id == 0: return True # 0号事务是初始插入,永远可见 if trx_id < self.min_id: return True if trx_id >= self.next_id: return False return trx_id not in self.active def main(): row = MVCCRow("金额=100") row.update(1, "金额=150") row.update(2, "金额=200") row.update(3, "金额=300") # 事务4和5还活跃:ReadView把它们加入活跃集 view = ReadView([4, 5]) print("事务3提交后,新事务看到的金额:", row.read_with(view)) # 全部提交后,新事务能看到最新值 view2 = ReadView([6]) print("没有活跃写事务时,看到的金额:", row.read_with(view2)) if __name__ == "__main__": main()
self.version_chain = [(0, init_value)]版本链用列表存储,初始版本事务ID为0,代表“上古版本”,永远可见。row.update(trx_id, new_value)每次修改直接追加到链尾,不覆盖旧值,体现多版本核心思想。for trx_id, value in reversed(self.version_chain)从链头(最新版本)开始往回遍历,模拟InnoDB的回溯过程。view.is_visible(trx_id)每到一个版本就调用一次可见性判断,等价于数据库中的ReadView检查。if trx_id < self.min_id: return True事务ID早于活跃集合下限的,说明早在快照之前就已提交,直接可见。if trx_id >= self.next_id: return False事务ID大于等于下一个分配ID的,是这个快照诞生之后才启动的“未来事务”,不可见。return trx_id not in self.active落在中间区间的事务,查活跃表:不在活跃表中说明已提交,可见。print("事务3提交后...")模拟事务1/2/3已提交、事务4/5还在进行的场景,观察旧版本仍可读到。
常见误区
误区既然MVCC这么厉害,读写互不阻塞,那是不是任何并发场景下都不需要加锁了?事务随便开、数据随便改都不会出错?
正解MVCC只解决了读-写并发和读-读并发,写-写冲突依然需要锁来解决。两个事务同时修改同一行时,后提交者必须等待或回滚。它相当于给读者发照片,让读者和写者各走各的路,但两个写者同时抢一支笔时,还是得排队。
为何常见:因为MVCC的广告效应太强,“读写不阻塞”容易让人误以为它包治百病,忽略了文档与教材中反复强调的当前读加锁细节。
误区可重复读(RR)隔离级别下,快照读总是不变地返回事务开始时的旧数据,别人提交的新数据永远看不到。
正解RR隔离级别下,快照读的ReadView在第一次SELECT时生成,之后确实复用;但事务刚开启、还没执行第一条SELECT时,如果先执行了当前读(如 UPDATE),当前读会读取最新数据并可能更新ReadView的生成时机。此外,在RC级别下每次SELECT都生成新快照,旧数据只存在于RR级别。快照并不是“永远旧”,而是“按规则决定新旧”。
为何常见:很多学习资料只举RR级别的经典例子,让读者把RR行为推广到了所有隔离级别,忽略了RC与RR在ReadView生成时机上的本质区别。
误区既然InnoDB有purge线程自动回收旧版本,那长事务也没什么大不了,数据库会自己处理那些旧底片。
正解purge线程只能清理“没有任何活跃ReadView引用”的版本。只要有一个长事务的ReadView还活着,它所能看到的所有历史版本都必须保留。长事务会让undo log持续膨胀,甚至撑爆磁盘、拖垮性能。这不是自动清理能兜底的场景。
为何常见:平时测试环境事务都很短,purge回收及时,给人“无需关心”的错觉;生产环境一旦出现跨小时的长事务,问题就集中爆发。
误区`SELECT ... FOR UPDATE` 也是读操作,应该和普通 `SELECT` 一样读可见版本,读不到就回溯到旧版本。
正解当前读不读历史版本,它只读最新版本并加行锁。MVCC的版本链可见性判断只服务于快照读。当前读的语义是“锁定最新状态并基于它修改”,如果读旧版本再加锁,就会导致更新丢失,违背隔离性的基本要求。
为何常见:初学者容易把所有SELECT都归类为“读”,忽略了 FOR UPDATE 修饰后语义已彻底变化——它更接近写操作的准备工作。
面试题
Q1. MVCC中,“多版本”指的是什么?
- A.数据库有多个副本存放在不同服务器
- B.同一行数据保留多个历史修改版本← 正确答案
- C.同一张表可以定义多个主键
- D.多个事务共享同一份数据快照
答案:B。MVCC的核心是每行数据通过版本链保留多个历史版本,供不同事务按需读取。选项A是分布式复制概念,C与索引相关,D的意思是多个事务共用快照,都不是多版本的本义。
Q2. InnoDB中,隐藏列 roll_pointer 的作用是什么?
- A.记录当前事务的隔离级别
- B.指向undo log中该行的上一个版本← 正确答案
- C.存储该行的主键索引指针
- D.标记该行是否被锁定
答案:B。roll_pointer 是版本链的“链条连接件”,指向修改前旧版本在undo log中的位置。它让数据库能沿链回溯历史版本,是MVCC版本链的物理基础。A是会话级属性,C是索引结构,D是锁系统的事。
Q3. 可重复读(RR)隔离级别下,事务内的快照读ReadView何时生成?
- A.每次执行SELECT语句时都重新生成
- B.事务内第一次SELECT时生成,之后复用← 正确答案
- C.事务开始时强制预生成
- D.每次UPDATE时重新生成
答案:B。RR级别的关键是“可重复读”,即同一事务多次读结果一致。因此ReadView在首次快照读时生成并复用整个事务期。RC级别才是每次SELECT都重新生成(选项A),UPDATE使用当前读并不依赖ReadView(选项D)。
Q4. 下列关于快照读与当前读的说法,正确的是?
- A.普通SELECT是快照读,不加锁← 正确答案
- B.SELECT …… FOR UPDATE是当前读,加排他锁← 正确答案
- C.快照读读到的一定是最新数据
- D.当前读读到的版本沿版本链回溯得到
答案:AB。普通SELECT走快照读,通过ReadView判断可见版本,不加锁(A对)。FOR UPDATE 是当前读,加排他锁并读最新版本(B对)。快照读未必读到最新数据(C错),当前读直接取链头最新版,不做回溯(D错)。
Q5. ReadView判断版本可见性时,事务ID小于 min_trx_id 的版本应如何处理?
- A.不可见,因为它是未提交的活跃事务
- B.可见,因为它早在快照生成前就已提交← 正确答案
- C.可见,但需要先获取行锁
- D.不可见,因为它属于未来事务
答案:B。min_trx_id 是快照生成时活跃事务中的最小ID。事务ID比它还小的,无疑在该事务启动前就已提交结束,属于“历史尘埃”,必然可见。A是中间区间且活跃才出现的情况,C与快照读无锁相悖,D是 max_trx_id 边界的情形。
Q6. MVCC中哪个隔离级别下不存在“不可重复读”问题?
- A.读未提交(RU)
- B.读已提交(RC)
- C.可重复读(RR)← 正确答案
- D.以上都存在
答案:C。RR级别复用首次ReadView,整个事务内的快照读结果一致,因此避免了不可重复读。RC级别每次SELECT都生成新ReadView,前后两次可能读到不同值,存在不可重复读。RU级别更是直接读最新未提交数据,问题更严重。
Q7. 关于长事务对MVCC的影响,以下描述正确的是?
- A.长事务会让旧版本无法被purge回收← 正确答案
- B.长事务会导致undo log持续膨胀← 正确答案
- C.长事务不影响快照读的性能
- D.长事务可能撑爆磁盘空间← 正确答案
答案:ABD。长事务的ReadView长时间存活,旧版本因此受到“保护”无法被purge回收(A对),undo log持续增长(B对),严重时可耗尽磁盘(D对)。同时版本链越拉越长,快照读回溯成本升高,性能显著下降,C说法错误。
Q8. MVCC主要解决了哪两类操作之间的阻塞?
- A.写操作与写操作
- B.读操作与写操作← 正确答案
- C.DDL与DML
- D.主从复制与主库写入
答案:B。MVCC的核心价值是让读操作不再阻塞写操作、写操作也不阻塞读操作,即读写互不阻塞。写-写冲突(A)仍由锁解决,C是DDL相关话题,D属于复制架构范畴,都与MVCC的目标不符。
Q9. InnoDB中purge线程的主要职责是什么?
- A.清理不再被任何活跃事务引用的旧版本← 正确答案
- B.回收被删除表占用的磁盘空间
- C.将日志刷入数据文件
- D.合并多个版本的索引页
答案:A。purge线程专门负责检查和清理版本链上“确认无人需要”的旧版本,释放undo空间。B是表空间回收,C是刷盘机制,D是索引维护,都不是purge的职责。
Q10. 下列哪些属于MVCC工程实践中的推荐做法?
- A.在事务中避免调用远程HTTP接口← 正确答案
- B.为高频更新表设计尽量多的索引
- C.监控活跃事务与undo增长趋势← 正确答案
- D.把大事务拆分为多个短事务← 正确答案
答案:ACD。事务中调用远程接口会拉长事务时长,增加undo积压风险,应避免(A对)。监控活跃事务与undo是发现隐患的常规手段(C对)。拆分大事务为短事务是MVCC实践的核心建议(D对)。索引过多会使更新维护成本倍增,应精简而非堆砌(B错)。