← 返回题目列表

PostgreSQL 的 MVCC 是怎么工作的?

高频 中等 第 3 / 31 题 更新于 2026/07/28
PostgreSQLMVCC事务并发控制

简化版

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 不是免费的。频繁 UPDATEDELETE 会产生大量旧版本,如果长期不清理,就会导致表膨胀、索引膨胀、扫描变慢。

常见风险包括:

  • 长事务一直不结束,旧版本无法回收;
  • 高频更新表产生大量 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 清理。回答时抓住“多版本、快照可见性、读写并发、清理成本”四个关键词,就能把机制和工程影响都讲清楚。