MVCC

MVCC 通过维护数据的多个版本与一致性快照,让数据库在很多场景下兼顾并发读取能力和事务隔离。

#type / concept #status / growing #tech / dev / backend #resource / database

[!info] related notes

MVCC

一句话定义

MVCC 是数据库通过维护同一行数据的多个版本和一致性快照,让读写在很多场景下不必彼此阻塞的并发控制机制。

它要解决什么问题

如果数据库只有“当前最新值”这一份数据,那么会很麻烦:

  • 写事务刚改完但还没提交,读事务到底能不能看
  • 读事务正在扫描大量数据时,写事务是不是都得等它结束
  • 想让同一个事务里多次读取看到稳定结果时,旧值从哪里来

MVCC 的目标就是:

  • 让读操作尽量不挡写
  • 让写操作尽量不因为普通读而停住
  • 让事务能根据自己的快照读到“它应该看到的版本”

核心机制 / 工作原理

1. 关键思想不是“改值”,而是“管理版本”

MVCC 的核心思路是:

  • 一条记录在逻辑上不只有一个值
  • 不同事务在不同时间点,可能各自看到不同版本

所以数据库不只是问“最新值是什么”,还要问:

  • 这个版本是谁写的
  • 这个写事务是否已提交
  • 当前读事务能不能看到它

2. 读事务通常基于某个一致性快照

很多实现里,事务在开始或第一次一致性读取时,会形成一个“可见性视图”。

之后它读取某一行时,数据库会判断:

  • 最新版本是否对当前事务可见
  • 如果不可见,是否需要回退到更旧的版本

这也是为什么同一事务里,即使其他事务已经提交了更新,你仍可能继续看到旧值。

3. 写事务不是把 MVCC 变成“无锁世界”

MVCC 主要改善的是读写冲突,不是取消一切协调。

通常仍然需要:

  • 用锁处理写写冲突
  • 防止两个事务同时改同一行导致覆盖
  • 在范围更新、结构变更或更强隔离级别下继续配合锁

所以更准确的说法是:

  • MVCC 降低了很多不必要的读写阻塞
  • 但它从不意味着“数据库不需要锁”

4. 旧版本通常离不开日志或版本链路

事务之所以能读到旧版本,不是凭空得到一份历史快照,而是数据库维护了相应的版本信息。

常见思路包括:

  • InnoDB 倾向于借助 Undo Log 还原旧版本
  • PostgreSQL 倾向于直接利用行版本和事务可见性信息判断是否可见

这也是为什么 MVCC 往往和日志、回收、清理机制紧密耦合。

最小例子 / 最小场景

事务 A 在更新一条订单状态,但还没提交时:

  • 事务 B 如果只是普通读取,通常仍能看到旧版本
  • 事务 B 不必一定因为 A 正在写而整条查询都停住
  • A 提交后,新开启的事务更可能看到新版本

这样数据库在高并发下更容易同时服务读取和写入。

从原理上最该抓住哪几层

1. MVCC 的核心不是“存多个值”,而是“定义版本可见性”

只有“旧版本存在”还不够,更关键的是:

  • 当前事务能看到哪个版本
  • 为什么能看到它
  • 什么时候这个旧版本可以被回收

2. MVCC 提升的是读写并发,不是无限并发

它最擅长的是:

  • 普通读和写同时存在时,减少彼此等待

它不擅长替代:

  • 写写冲突控制
  • 死锁处理
  • 所有更高隔离需求

3. MVCC 往往把问题从“阻塞”转移到“版本管理”

好处是:

  • 读操作更顺滑

代价是:

  • 需要维护版本链
  • 需要清理旧版本
  • 长事务会拖慢版本回收

这也是为什么长事务在 MVCC 系统里经常是性能与空间上的麻烦制造者。

边界与易混淆点

  • MVCC 不是“完全不用锁”。写写冲突、元数据变更和某些隔离级别下仍然需要锁。
  • MVCC 不是所有数据库都以同样方式实现,理解差异时要回到具体产品。
  • MVCC 解决的是一部分并发可见性问题,不等于自动解决所有幻读、死锁和一致性问题。
  • MVCC 也不是“谁都能永远看到旧版本”。旧版本的保存和回收都受事务生命周期约束。
创建于 2026/5/3 更新于 2026/5/27