PostgreSQL 的 MVCC,平常最容易被记成一句让人安心的口号。读不阻塞写,写不阻塞读,查询总能看见一份一致视图。这些说法都成立,官方文档也确实这样写。[3] Bruce Momjian 这场 MVCC Unmasked 演讲真正有用的地方,在于它不肯把 MVCC 留在口号层面。整场演讲反复把并发拉回到物理存储:旧元组版本如何保留下来,新版本如何接替它,快照怎样决定哪一层历史算“现在”,而清理工作为什么必须随后跟上,不然整套系统很快就会堆满失效元组。[1][2][5]

把这支视频放到 2026 年再看,这种读法依旧较稳。PostgreSQL 文档今天仍然把 MVCC 解释成一种以快照为中心的并发方法,再把事务隔离与 routine vacuuming 分开成不同章节。[3][4][5] Momjian 的演讲做的事情,恰好是把这些章节重新接回去。听到最后,vacuum 已经不再像一条躲在附录里的维护命令,它成了并发模型内部的一部分。PostgreSQL 之所以能给出平顺的读写体验,正因为它愿意暂时保留多份行版本,再用非常严格的清理机制把这些历史收回去。[1][2][5]

顺着视频、讲义与官方文档一起读,我更愿意把 PostgreSQL 的 MVCC 理解成一份“先版本化,再清理”的契约,而并非一句“没有锁”的神奇承诺。[1][2][3][5] 锁当然仍然存在,需要更强隔离级别的时候边界也很明确。[3][4] PostgreSQL 在普通事务负载里显得从容,靠的并非取消代价,而是把逻辑上的修改改写成物理上的元组版本变化,再用快照、HOT update 与 vacuum 把这份代价控制住。

配图说明:题图使用 Bruce Momjian 个人主页上的真实会议照片,页面注释为 Moscow, 2007。它适合本文,因为文章围绕的是一位讲者对 PostgreSQL 内部机制的拆解,并非库存式仪表盘,也并非抽象数据库图标。[6]

大约从 2:58 开始,Momjian 先解释了为什么 MVCC 需要“卸妆”

演讲一开始的比喻很巧。Momjian 放出一张《星球大战》演员脱离角色装束后的照片,说一旦知道外壳下面是什么,再去看电影,眼睛会变得不一样。[1] 这正是 MVCC 这类题目最需要的入口。多数 PostgreSQL 用户每天都在使用 MVCC,真正缺少的往往并非定义,而是一幅足够具体的内部图像:一次 UPDATE 在系统里究竟意味着什么。

讲义很快把这件事画实了。插入、更新、删除,并非三件彼此分离的事件,而是元组版本被创建、被标记失效、再被下一版本接续的不同形态,元组本身带着创建与过期信息。[2] 这个视角一旦建立,MVCC 就不再只是逻辑并发特性,它开始像一套带着长期后果的存储策略。所谓更新,不再只是“这一行改了”,而是“旧版本被标记为过期,新版本被推成候选可见状态”。这条路更重,但它也正是 PostgreSQL 能让旧读者维持稳定、让新写者继续推进的原因。

大约从 8:20 开始,那句最著名的口号被放回结果位置,而并非机制位置

演讲最重要的一个转折,出现在 Momjian 重新提到那句熟得不能再熟的话:读不阻塞写,写不阻塞读。[1][3] 这句话很容易记,也很容易被误读。它会让人误以为 PostgreSQL 凭空消除了并发与一致性之间的代价。官方文档写得更准确:PostgreSQL 真正提供的是快照可见性,也就是一条 SQL 语句看到的是某个时刻的数据库版本,而并非扫描经过每一行时都去索取“全局最新状态”。[3]

这层差别很关键。PostgreSQL 并没有把同一个“现在”发给所有读者,它给每个读者的是一份时间切片清楚、内部自洽的历史横截面。[3][4] 读写之间那种不互相绊住的体感,来自这种时间上的绕行。读者之所以能继续向前,是因为它不要求看到最新的在途版本;写者之所以能继续向前,是因为它生成后继版本,而并非把共享现实原地改写。那句口号因此只是用户感受到的结果,真正的机制则是“版本增殖,加上可见性规则”。

大约从 10:23 到 12:23,快照何时产生,几乎就讲完了整件事

等 Momjian 开始解释 snapshot 到底做什么、PostgreSQL 又在什么时候记录 snapshot,演讲就真正进入核心。[1] 在 Read Committed 里,每条 SQL 语句开始时都会取一次快照;在 Repeatable ReadSerializable 里,快照是在事务开始时取得的。[1][4] 官方事务隔离章节把这几点写得同样清楚,也把后果说明白了:Read Committed 里的连续查询或许看见不同的已提交结果,Repeatable Read 会维持事务级稳定视图,Serializable 则通过 Serializable Snapshot Isolation 换来更强边界,同时引入需要重试的失败情形。[4]

很多应用侧错误,恰恰发生在这里。开发者听见“PostgreSQL 是基于 MVCC 的”,就顺手把“事务”理解成“自动拥有一份固定现实”。只有更强隔离级别才真是这样。[4] Momjian 这段说明更有价值的地方,在于它把抽象又带回了存储压力。快照需要维持多久,那些旧元组版本就或许需要保留多久。一条跑很久的报表事务,因此不只是查询规划问题,它同时也是一次清理延迟。

大约从 16:06 开始,又在 20 分钟以后继续展开,xmin 与 xmax 让行历史变成可被检查的东西

整场视频里最能把人拉进系统内部的部分,是 Momjian 从概念转向元组头部的时候。他展示创建事务如何记在 xmin,过期事务如何记在 xmax,然后配合 pageinspect 与 free-space map 的演示,让观众直接看见版本变化,而并非只在脑子里想象它。[1][2] 讲义里关于插入、更新、删除、游标与 tuple state 的示例,把这层关系写得非常具体。[2]

这件事重要,是因为 PostgreSQL 的 MVCC 并非靠一个看不见的全局 undo 神话撑起来的。它靠的是带着历史标记的行版本,再把这些标记放进某个 snapshot 的语境里解释。[1][2] 这层认识一旦落地,许多运维建议就不再像仪式。为什么要避免闲置长事务,为什么大批量更新会留下明显尾迹,为什么写密集表会在清理追上之前积累 dead tuples,这些都并非经验主义口号,而是版本化存储模型直接推出的结果。[5]

大约从 34:35 与 39:32 开始,vacuum 与 HOT update 终于不再像可有可无的善后动作

这场演讲后半段最值得反复看的,就是清理部分。Momjian 解释 PostgreSQL 必须处理被删除的行和失效的旧版本,而 autovacuum 或手动 vacuum 会在这些元组对所有活动快照都不可见之后接手这份工作。[1] PostgreSQL 文档也把同样的逻辑写得更直白:UPDATEDELETE 并不会立刻抹掉旧行版本,routine vacuuming 的职责既包括回收空间,也包括避免 transaction ID wraparound 这种更底层的故障。[5]

视频最有分量的地方,在于它没有把这一切压成一句“vacuum 会清垃圾”。讲义把 page-level cleanup、free space reuse,以及普通 vacuum 与 VACUUM FULL 之间的边界都摆到了台面上。[2][5] 之后 Momjian 又提到 HOT update:如果被更新的列不影响索引,PostgreSQL 就能尽量减少索引层面的额外震荡。[1][2] 真正应该带走的理解也在这里。PostgreSQL 从来没有免费得到并发能力,它只是把代价分摊成持续、渐进、足够便宜的清理工作,让系统能在日常运转里把历史负担慢慢吞下去。

因此,“清理契约”比“无阻塞口号”更值得记住。若只记得那句口号,PostgreSQL 的膨胀问题总会看起来像一桩无关紧要的维护杂务。把视频认真看完之后,bloat、autovacuum 阈值、长寿命快照与 HOT update 的意义会重新并到同一条线上。[1][2][5] PostgreSQL 的并发之所以成立,是因为它愿意把过去保留到足够诚实,再把这段过去清掉到足够克制,既不让快照失真,也不让 OLTP 负载被自己留下的历史拖住。

来源

  1. PGConf India,《PGConf India 2024 - MVCC Unmasked by Bruce Momjian (EDB)》,YouTube 视频,发布于 2024 年 3 月 23 日。
  2. Bruce Momjian,《MVCC Unmasked》讲义 PDF,最后更新于 2026 年 2 月。
  3. PostgreSQL 18 Documentation,《13.1. Introduction》——MVCC 总览、快照视图与读写互不阻塞的说明。
  4. PostgreSQL 18 Documentation,《13.2. Transaction Isolation》——Read Committed、Repeatable Read 与 Serializable Snapshot Isolation 的行为边界。
  5. PostgreSQL 18 Documentation,《24.1. Routine Vacuuming》——dead row 清理、autovacuum 与 wraparound 防护。
  6. Bruce Momjian,个人主页——本文配图来源页面,页面注释为 Moscow, 2007。