连载中 8/22

隔离级别与 MVCC:快照读到底读的是什么

2026-05-05 · 6681 阅读 · 0 评论 · 0 赞

三个经典怪象

老王的对账脚本最近天天出鬼:同一个事务里两次查询余额,数字不一样;运营报表里偶尔出现一条取消事务里「没存在过」的订单;还有对账统计偶尔多算一笔。三个怪象,正好对应事务并发的三大问题:脏读、不可重复读、幻读。这篇把隔离级别和 MVCC 讲透,怪象自然消解。

三大并发问题与四个级别

问题现象
脏读读到了别的事务还没提交的数据,对方回滚后这就是幻觉数据
不可重复读同一事务内两次读同一行,值变了(别人提交了 update)
幻读同一事务内两次按相同范围查询,行数变了(别人提交了 insert)

SQL 标准给出四个隔离级别,隔离性依次增强、并发性能依次减弱:

级别脏读不可重复读幻读
读未提交 RU可能可能可能
读已提交 RC杜绝可能可能
可重复读 RR(InnoDB 默认)杜绝杜绝快照读杜绝
串行化杜绝杜绝杜绝

注意 MySQL 的 RR 对幻读的表述是「快照读下杜绝」——当前读场景要靠间隙锁,这是第 9 篇的话题,这里先埋一句。

MVCC 的骨架:版本链

InnoDB 的每行记录都有两个隐藏列:trx_id(最后修改它的事务 ID)和 roll_pointer(指向上一个版本的指针)。每次 update 都把旧版本压进 undo log,用 roll_pointer 串成一条版本链——第 7 篇说的 undo 第二职责,就是这条链。

ReadView:给可见性画一条线

事务读数据时生成一个 ReadView,核心四个字段:当前活跃(未提交)事务 ID 列表 m_ids、其中最小的 min_trx_id、下一个将分配的事务 ID max_trx_id、自己的 creator_trx_id。拿到一个版本,判断规则四条:

  • 版本的 trx_id == 自己 → 自己改的,可见;
  • trx_id < min_trx_id → 改它的事务早已提交,可见;
  • trx_id >= max_trx_id → 改它的事务在我快照之后才开始,不可见;
  • 在两者之间 → 查 m_ids:还活跃着(没提交)不可见,已提交可见。

当前版本不可见,就顺 roll_pointer 找上一版本再判断——直到找到第一个可见版本。所谓快照读,就是「顺着版本链找出对我可见的那个版本」

RC 和 RR 的唯一区别

就一条:RC 每次执行查询都生成新的 ReadView;RR 只在事务第一次读时生成一个,整个事务复用。RR 复用同一份快照,所以两次读同一行结果必然相同(不可重复读被杜绝);RC 每次看最新已提交版本,所以能读到别人新提交的修改。互联网公司常有把默认改成 RC 的(间隙锁少、死锁率低),但 RR 是默认值且语义更稳——没有特殊理由别改。

回头看老王的三个怪象:对账脚本数字对不上,是代码里没把两次查询包进同一事务(或用了 RC 语义);报表出现幻影订单,是读了未提交数据(RU)或统计逻辑没对齐隔离级别;多算一笔则是范围统计撞上了并发 insert。按 RR + 明确事务边界重构后,全部消失。

MVCC 解决「读」,但两个事务同时改一行怎么办?这就是锁的地界:行锁、间隙锁、next-key lock,以及一场真实的死锁事故复盘——下一篇见。

咖啡凉了,记得趁热喝。

503

10 年全栈工程师 · 503咖啡馆主理人

#MySQL#隔离级别#MVCC#ReadView#幻读

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞