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_metrics 或 SHOW 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% 的"读不到/读得到"问题,也能设计出不踩长事务坑的业务。