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 也不是“谁都能永远看到旧版本”。旧版本的保存和回收都受事务生命周期约束。