PostgreSQL 的 MVCC 是怎么工作的?
简化版
PostgreSQL 的 MVCC 是多版本并发控制。它不会让读写简单互相阻塞,而是在更新数据时生成新的行版本,旧版本暂时保留给已经开始的事务读取。事务通过快照判断哪些版本可见,提交后的新版本对后续事务可见,过期的旧版本再由 VACUUM 清理。
详细版
MVCC 的核心是“同一行数据可以在一段时间内存在多个版本”。PostgreSQL 每个行版本里有事务相关元信息,常见可以理解为:
xmin:创建这个行版本的事务 ID;xmax:删除或替换这个行版本的事务 ID;- 快照:事务开始或语句开始时看到的活跃事务集合。
当执行 UPDATE 时,PostgreSQL 通常不是原地覆盖旧行,而是把旧行标记为不再对新事务可见,并写入一个新行版本。老事务如果快照还需要旧版本,就继续读旧版本;新事务则可能读到新版本。
这样做的好处是读写并发能力强,普通查询通常不会被正在修改的事务直接挡住。但代价是会产生旧行版本,也就是 dead tuples,需要 VACUUM 回收空间并维护统计信息。
完整版教学
一、MVCC 解决的核心问题
数据库并发里有一个经典矛盾:如果所有读写都靠锁串行化,安全是安全了,但并发性能会很差。MVCC 的思路是把“正在被别人修改的数据”变成多个可见版本,让不同事务按照自己的快照读取合适的版本。
例如事务 A 已经开始查询用户余额,事务 B 同时更新余额并提交。事务 A 是否能看到新余额,取决于 A 的隔离级别和快照时机,而不是简单地等 B 解锁。
二、PostgreSQL 更新数据时会留下旧版本
假设一行用户数据原来是:
id = 1, name = 'Tom'
执行:
UPDATE users SET name = 'Jerry' WHERE id = 1;
PostgreSQL 会创建一个新行版本:
旧版本:id = 1, name = 'Tom'
新版本:id = 1, name = 'Jerry'
旧版本不是立刻物理删除,因为可能还有早已开始的事务需要读取它。等确认没有事务再需要这个旧版本时,VACUUM 才能清理。
三、可见性由事务快照决定
快照可以理解为“我这个事务或语句开始时,哪些事务已经提交,哪些事务还没结束”。读取一行时,PostgreSQL 会根据行版本的事务信息和当前快照判断它是否可见。
一个行版本通常要满足两个方向:
- 创建它的事务已经提交,并且对当前快照可见;
- 删除或替换它的事务还没有对当前快照生效。
所以并不是磁盘上最新的行版本就一定能被当前事务读到,关键是它对当前快照是否可见。
四、MVCC 带来的好处
MVCC 最大的好处是提升读写并发:
- 普通
SELECT可以读取快照中的版本,不一定被写事务阻塞; - 写事务可以生成新版本,不必等待所有读事务结束;
- 不同隔离级别可以通过不同快照策略实现一致性要求;
- 事务回滚时,可以利用版本信息恢复逻辑可见性。
这也是 PostgreSQL 在高并发业务系统里常被问到 MVCC 的原因。
五、MVCC 的代价是膨胀和清理
MVCC 不是免费的。频繁 UPDATE、DELETE 会产生大量旧版本,如果长期不清理,就会导致表膨胀、索引膨胀、扫描变慢。
常见风险包括:
- 长事务一直不结束,旧版本无法回收;
- 高频更新表产生大量 dead tuples;
- Autovacuum 配置不合理,清理跟不上写入速度;
- 统计信息不及时,优化器选择错误执行计划。
所以 PostgreSQL 面试里,MVCC 经常会和 VACUUM、长事务、表膨胀、事务隔离级别一起考。
六、常见误区与追问
| 现象 | MVCC 里的原因 | 工程影响 |
|---|---|---|
| 普通读不阻塞写 | 读旧快照版本 | 提升并发 |
| UPDATE 后表变大 | 新旧行版本并存 | 需要 VACUUM |
| 长事务导致膨胀 | 老快照还需要旧版本 | dead tuples 不能回收 |
易错点:MVCC 不是“没有锁”。它让普通快照读少等写锁,但写写冲突、显式锁、DDL、唯一约束检查仍然会产生等待和冲突。
用时间线看可见性:
T1: 事务 A 开始,看到 name='Tom'
T2: 事务 B UPDATE name='Jerry' 并提交
T3: 事务 A 再读,同一快照下仍可能看到 'Tom'
T4: 新事务 C 开始,看到 'Jerry'
这里磁盘上可能已有新版本,但 A 的快照决定它读旧版本。等 A 结束后,如果没有其他事务需要旧版本,VACUUM 才能回收旧行版本。
- 误区:MVCC 会立刻删除旧数据。 旧版本要等没有活跃快照需要时才能被 VACUUM 清理。
- 误区:MVCC 让所有操作都不加锁。 快照读更少阻塞,但写写冲突、行锁、表锁和 DDL 锁仍然存在。
- 误区:看到最新行版本就等于当前事务可见。 可见性由事务快照和版本事务信息判断,不是单纯看物理最新。
- 追问:长事务为什么会导致表膨胀? 长事务持有旧快照,VACUUM 不能清理它可能还要读取的旧版本。
- 追问:MVCC 和隔离级别是什么关系? Read Committed 通常每条语句一个快照,Repeatable Read 通常事务级稳定快照。
- 追问:PostgreSQL MVCC 的代价是什么? UPDATE/DELETE 产生旧版本,带来表膨胀、索引膨胀、VACUUM 和统计信息维护成本。
七、加强记忆
PostgreSQL MVCC 的主线是:更新生成新版本,旧版本留给旧快照,事务按快照判断可见性,过期版本交给 VACUUM 清理。回答时抓住“多版本、快照可见性、读写并发、清理成本”四个关键词,就能把机制和工程影响都讲清楚。