三个「读异常」到底在怕什么

并发事务下,数据库可能出现三种读异常:脏读(读到别人还没提交的数据——别人回滚了你手里就是错的)、不可重复读(同一个事务内两次读同一行结果不同——中间有人改了并提交了)、幻读(同一个事务内两次同一范围查询行数不同——中间有人插入了新行)。
隔离级别的全部意义,就是用「性能」换「这三种异常的出现与否」——读未提交(都有)→ 读已提交(解决脏读)→ 可重复读(再加解决不可重复读)→ 串行化(全解决但性能崩溃)。MySQL 的默认级别是可重复读(RR)——而 RR 对幻读的处理是「半解决」——这个「半」字的机制,就是这篇的核心。
MVCC:不加锁的并发读是怎么做到的
MySQL InnoDB 实现 RR 级别的核心机制是 MVCC(多版本并发控制)——不靠锁,靠「多版本」:
undo log 与数据的多版本。InnoDB 的每行记录都有隐藏字段:DB_TRX_ID(最后修改这行的事务 ID)与 DB_ROLL_PTR(回滚指针,指向 undo log 中的旧版本)。当一个事务修改一行时,旧版本被存入 undo log,新版本带着新的事务 ID 写在主记录上——一行数据在存储里是一条「版本链」:最新版本 → 回滚指针 → 上一版本 → 再上一版本……直到最早的版本。读旧数据不需要锁:要读旧版本,沿着版本链往回走就行——这就是「不加锁的并发读」的实现基础。
Read View:可见性的裁判。事务开始时(RR 级别是第一次读时),InnoDB 创建一个 Read View——一个「当时系统状态的快照」,包含四个关键信息:m_ids(生成视图时系统中活跃(未提交)的事务 ID 列表)、min_trx_id(活跃事务中的最小 ID)、max_trx_id(下一个将要分配的事务 ID)、creator_trx_id(创建这个视图的事务自己的 ID)。可见性判断的规则:沿版本链从新到旧,检查每个版本的 trx_id——如果这个版本的事务 ID 等于自己的 ID(自己改的,可见);小于 min_trx_id(生成视图前已提交,可见);大于等于 max_trx_id(生成视图后才开启的事务,不可见);在 min 与 max 之间——查 m_ids:在活跃列表里(当时还没提交,不可见),不在(当时已提交,可见)。第一个满足可见性的版本就是这次读的结果——这就是 RR 级别「可重复读」的机制:Read View 在第一次读时创建、整个事务复用——所以整个事务期间「别人提交了什么」都与你的视图无关。
graph TD
A[MVCC 双组件] --> B[undo log 版本链<br/>一行数据的多版本历史]
A --> C[Read View 快照<br/>活跃事务列表 + 边界 ID]
C --> D[可见性规则<br/>自己改的→可见<br/>视图前已提交→可见<br/>活跃或未来的→不可见]
B & D --> E[RR 级别: 视图首读创建 整事务复用<br/>RC 级别: 每次读创建新视图]
RC 与 RR 的一字之差
读已提交(RC)与可重复读(RR)的唯一实现差异:Read View 的创建时机——RC 级别每次读都创建新的 Read View(别人提交的更新,下次读就能看到——所以「不可重复读」会发生);RR 级别第一次读创建、整事务复用(别人提交了也看不到——所以「可重复」)。一行代码的差异(概念上),两个完全不同的隔离体验。
为什么很多公司把隔离级别调成 RC:互联网公司的常见操作(SET GLOBAL transaction_isolation = 'READ-COMMITTED')——理由是 RC 的锁冲突更少(RR 级别的间隙锁是幻读防御的一部分——锁的范围更大、并发冲突更多——RC 没有间隙锁(大部分场景)——锁粒度更细、死锁更少——互联网业务对「不可重复读」的容忍度(一个事务内两次读同一行结果不同——很多业务场景不在乎(反正每次读的都是最新值)——「最新值优先」的业务哲学——RC 的选择是业务权衡而非技术缺陷)。
RR 级别的幻读为什么是「半解决」
快照读解决了幻读:普通 SELECT(快照读)走 MVCC——Read View 不变,别人插入的新行不可见——幻读在快照读下不存在。当前读的幻读仍在:当前读(locking read)——SELECT ... FOR UPDATE、UPDATE、DELETE 这些「读最新版本+加锁」的操作——它们不走 Read View,读的是最新已提交的数据——如果别的事务在两次当前读之间插入了一行,你的当前读会看到它——幻读在当前读下仍会发生。
间隙锁的补丁:RR 级别的防御是间隙锁(Gap Lock)——当前读时不仅锁记录,还锁记录之间的「间隙」——别的事务不能在锁定的间隙里插入新行——「记录锁+间隙锁=临键锁(Next-Key Lock)」——这就是 RR 的幻读防御:当前读时用临键锁把「范围」锁住,别人的插入进不来。「半解决」的准确含义:快照读(无幻读)+ 当前读(间隙锁防御幻读)——但混合使用时仍可能「看到幻影」(先快照读(看不到新行)、再当前读(看到了——同一个事务内两种读的结果不一致——这就是「幻读半解决」的真相——理论上 RR 还有幻读的残影,实践中需要正确使用「一致读」模式(整个事务都用快照读或都用当前读)才能彻底避免。
RR 的代价与选择:间隙锁的并发代价(锁的范围大——热点区间的插入冲突——死锁的概率上升——RR 的安全性是用锁的粒度买的——这也是很多互联网公司切 RC 的原因——RC + 业务层处理「不可重复读」(在真正需要一致读的地方显式加锁或用乐观锁——把一致性控制从数据库层上移到业务层——精细但复杂——RC 与 RR 的选择没有标准答案,是并发模型与业务语义的匹配。
MVCC 的进阶:写操作与锁的配合
MVCC 之外,写操作与锁的配合是另一半。写操作的锁行为:UPDATE 与 DELETE 走当前读(当前读需要加锁——「行锁」的粒度(锁的是索引项——「没有索引的更新」的灾难(WHERE 条件无索引——锁全表——「更新的 WHERE 必须有索引」的纪律——「锁的排他性」(X 锁——写锁与 MVCC 快照读的兼容(写不阻塞快照读——「读写不冲突」的 MVCC 红利——写与写互斥——「写-写冲突」的死锁源——「死锁」的检测(innodb_deadlock_detect——「死锁的主动检测」——「死锁的预防」:按相同顺序加锁——「事务大小」的控制(事务越小、锁持有越短、死锁概率越低——「大事务拆小」的纪律。「RR 级别的间隙锁」的死锁贡献:RR 的间隙锁比 RC 多——死锁概率高——「切 RC」的死锁改善——「间隙锁」的关闭(innodb_locks_unsafe_for_binlog 的历史选项——「禁止」的提醒(关闭间隙锁破坏主从一致性——「主从一致」的代价——「间隙锁」是安全与并发的交换。「MVCC 的性能影响」:版本链的长度(长事务导致版本链长——「长事务的毒性」(版本链长=回溯成本高——「长事务的监控」(information_schema.innodb_trx——「长事务的清理」——「purge 的滞后」(undo log 的清理依赖「最老的 Read View」——「长事务阻塞 purge」——「undo 表空间的膨胀」——磁盘的暴涨——「长事务」是 MVCC 系统的第一大敌——「事务的时限」(innodb 的 max_execution_time——应用层的强制超时——「事务超时」的强制设置。「MVCC 的调试与观测」:观察版本链的深度(「历史列表长度」(History list length——SHOW ENGINE INNODB STATUS——「历史列表的监控」——历史列表长=有长事务或 purge 滞后——「告警的阈值」。MVCC 的进阶总结:MVCC 解决「读-写并发」,锁解决「写-写并发」——两者配合,而「长事务」是两者的公敌——版本链变长、purge 滞后、锁持有变久——「消灭长事务」是 MVCC 系统运维的第一优先级——事务时限、监控告警、死锁预防——三件套让 MVCC 系统健康运转。
MVCC 高频问答
收尾前回答三个高频问题。「RR 下 UPDATE 读的是旧值还是新值?」——当前读读新值:UPDATE 的 WHERE 走当前读(最新已提交版本)——「快照读与当前读的分界」:SELECT 走快照、UPDATE/DELETE/FOR UPDATE 走当前——「同一事务混用」的坑(先 SELECT 判断再 UPDATE——判断基于快照、UPDATE 基于最新——判断与更新的版本不一致——「SELECT FOR UPDATE 再判断」的正确姿势(要基于最新值做判断就用当前读——「先锁再查」的模式。「快照读能读到自己的修改吗?」——能:「自己的修改总是可见」(Read View 的 creator_trx_id 规则——版本链上自己改的版本可见——「事务内读到自己的写」符合直觉——「写后读」的一致性(事务内——「跨事务的写后读」才有问题(读已提交其他事务的写——「读写一致性」的边界。「MVCC 解决幻读了吗?」——精确回答:快照读下解决、当前读下靠间隙锁、混用时有残影——「幻读的三种情况」:纯快照读(无幻读)、纯当前读(间隙锁防)、先快照后当前(有残影)——「一致性读模式」的建议(事务内统一用快照读或统一用当前读——「统一」是消除残影的方法。MVCC 的问答收束:MVCC 的心智模型一句话——「读的是你开始时拍的照片,写的是别人看不到的草稿,提交后才对别人可见」——RR 是「一张照片用到底」,RC 是「每拍一张新的」——「照片」的比喻能让团队快速建立共识——以及,**长事务=照片太旧=undo 膨胀——「短事务」是 MVCC 的养生之道。
小结
MySQL 事务隔离的机制地图:MVCC 的双组件(undo log 版本链——一行数据的多版本历史;Read View——活跃事务快照与可见性规则的四分支判断);RC 与 RR 的一字之差(视图创建时机——每次读新建 vs 首读复用——决定了不可重复读的有无);RR 幻读的「半解决」(快照读无幻读、当前读靠间隙锁防御、混合使用仍有残影——彻底避免需要一致的读模式);RC 的工程选择(无间隙锁、锁冲突少——互联网公司的主流选择——一致性的控制上移到业务层)。检验理解的一句话:RR 下同一个事务,先 SELECT 再 SELECT FOR UPDATE,第二次可能看到第一次看不到的行——能解释为什么,MVCC 才算真的懂了。
隔离级别不是「越高越好」的参数,是「一致性需求与并发性能」的调节旋钮——转它的手,应该懂它背后的 undo log 与 Read View。
读者留言
COMMENTS 暂无还没有留言,来说第一句?