MySQL MVCC 与隔离级别:读懂 InnoDB 的读一致性

“为什么我在 RR(可重复读)隔离级别下,读不到别人刚提交的数据?““为什么 A 事务删了 100 行,B 事务还能查到?"——这些问题的答案都在 MVCC(多版本并发控制)里。

一、MVCC 要解决什么

并发事务读写同一行,如果没有控制,会出现:

  • 脏读:读到别人未提交的数据
  • 不可重复读:同一查询两次结果不同(别人提交了)
  • 幻读:同一范围查询两次,行数变了

MVCC 的思路:读不阻塞写、写不阻塞读——每个事务看到的是"自己的快照”,通过版本链实现,而不是靠锁硬排队。

二、InnoDB 的隐藏列

InnoDB 每行有 3 个隐藏列:

D D D B B B _ _ _ T R R R O O X L W _ L _ I _ I D P D T R I D u I n D d o l o g

关键:DB_ROLL_PTR 串起来的版本链——同一行的多个版本通过回滚指针连成链表,旧版本数据存在 undo log 里。

v v v 3 2 1 D B D _ B T _ R D T X B R _ _ X I T _ D R I = X D 1 _ = 0 I 5 0 D 0 , = , 8 D 0 B , D _ B R _ O D R L B O L _ L _ R L P O _ T L P R L T _ R P T v R 2 N U L v L 1

三、Read View:事务的快照

事务启动(第一次快照读)时生成 Read View,记录:

c m m m r _ i a e i n x a d _ _ t s t t o r r r x x _ _ _ t i i r d d x _ i d I I D D I D I D

判断一行版本是否可见(核心规则):

1 2 3 4 . . . . m - - t i r n x t t _ _ r r t m i x x r _ m d _ _ x i _ i i _ d i = d d i s d = d s < > c = < r m = e i m a n a t t _ x r o t _ x r r t _ _ x r i t _ x d r i _ x d i < _ d i m d a 沿 x _ t r x _ i d

四、隔离级别的 MVCC 差异

隔离级别 快照读时机 效果
READ COMMITTED 每次查询生成新 Read View 能读到别人新提交的(不可重复读)
REPEATABLE READ 事务首次查询生成 Read View,之后复用 整个事务看到同一快照(可重复读)

这就是"RR 下读不到别人刚提交的数据"的原因:Read View 是事务开始时定格的,后续查询都用同一个视图,别人提交了对它不可见。

幻读的残余问题

RR 的快照读解决了"查询结果不变”,但当前读SELECT ... FOR UPDATE / UPDATE / DELETE)走的是另一条路:当前读 + 间隙锁

R R F O R S E U L P E D C A T T E M V C C +

间隙锁:锁定一个范围(比如 age BETWEEN 20 AND 30),阻止其他事务在这个范围插入——这就是 RR 防幻读的机制(牺牲部分并发)。

五、undo log 与版本链的代价

MVCC 不是免费的:

u n d o l o u g n d o p u r g e 沿

实践

  1. 长事务是 MVCC 的头号杀手:一个事务开 10 分钟,undo 不能清理,版本链膨胀,同表其他查询全变慢
  2. 监控 undo 大小information_schema.innodb_metricsSHOW ENGINE INNODB STATUS
  3. 避免高频更新同一行:热点行版本链巨长,查询性能雪崩

六、实战问答

Q:A 事务改了 100 行未提交,B 事务 SELECT 为什么看不到? A:B 的 Read View 里 A 在 m_ids(活跃)中,沿版本链找到旧版本 → 看不到新值。这就是"读不阻塞写"的体现——A 没提交,B 读到旧快照,双方互不阻塞。

Q:为什么 RR 下 UPDATE 会锁冲突? A:UPDATE 是当前读,走索引锁 + 版本链更新。两个事务 UPDATE 同一行,后到的要等前一个提交——写写冲突还是要等锁,MVCC 只解决读写互不阻塞

总结

MVCC 的完整认知:

  1. 版本链:隐藏列 + undo log,一行多版本
  2. Read View:快照读的可见性判断规则
  3. RR 靠"复用 Read View”,RC 靠"每次新视图"
  4. 幻读:当前读靠间隙锁兜底
  5. 代价:长事务 = 版本链膨胀 = 性能杀手

MVCC 的本质是用空间(多版本)换并发(读写不阻塞)。 理解它,你就能解释 90% 的"读不到/读得到"问题,也能设计出不踩长事务坑的业务。