多版本并发控制让 PostgreSQL 保留不同的行版本,再判断查询可以看到哪些版本。普通读者可以继续读取较早已提交的版本,同时另一个事务写出新版本。这减少了读写之间的冲突,但不表示每个事务都永久拥有一幅固定画面。快照寿命取决于隔离级别,而且事务能看到自己此前的修改,即使其他事务尚看不到。把这些边界说清楚,才能理解读取结果为何既稳定又可能变化。
可见性跟随快照
在读已提交级别,普通读取通常每条语句取得一个新快照。PostgreSQL 的可重复读则复用事务第一条非事务控制语句建立的快照。仅仅发送 BEGIN,还没有建立实际读取使用的边界。因此,一个较早开始、较晚才第一次读取的事务,可能看到这两个时刻之间已经完成的提交。“事务启动时间”与“读取快照已经建立”不能随意混用。
更新会创建一个新行版本。可见性规则决定旧快照是否仍读取之前的版本,而新快照是否看到更新。元组物理上仍然存在,并不足以说明当前查询能看到它;SQL 也不是直接返回数据页上最新写下的字节。只有结合事务状态与快照,才能判断哪一版记录属于这次读取的结果。
事务自己的修改,是简单“历史照片”比喻的重要例外。即使快照早已建立,事务仍然可以读到之后由自己插入的记录。它看到的数据,可能同时包含其他事务的较早状态与自己的新工作。这种行为让事务能够实际完成读写操作,但不能把它误解为外部世界某一个已提交时刻的完整照片。理解这个例外,也能避免错误地认为自己的更新必须提交以后才可读。
坏掉的样子
普通读取取得的常规表锁不会阻塞正常行更新,但锁并没有消失。两个写者仍可能争用同一行;SELECT FOR UPDATE 会明确请求行锁;ACCESS EXCLUSIVE 表锁则与普通读者冲突。因此,MVCC 不承诺所有带有 SELECT 的命令都永远不等待。是否阻塞还需要看语句请求的锁以及当前其他操作。
旧快照还可能让读者需要的旧行版本继续保留。只要某个相关快照仍可能看到该版本,清理就不能直接移除它。空间压力取决于更新量、保留版本和清理进度,不只是打开了多少个事务。再启动一次清理,并不会让仍然活跃的快照忘掉自己的可见性要求;需要沿保留原因调查,而不是只盯住维护命令有没有运行。
快速验证
历史双会话测试保持原样,本次没有重跑。单用户后端不能证明并发快照行为。
在同一个可丢弃的 PostgreSQL 18 数据库中,打开甲、乙两个新的 psql -X -v ON_ERROR_STOP=1 会话,使用相同角色,显式事务之外保持正常自动提交。模式名称必须尚未被使用。按下面顺序执行,让乙在甲的读事务仍然打开时完成提交。
- 在乙中建立一条已经提交的原始记录。
CREATE SCHEMA mvcc_visibility_demo;
CREATE TABLE mvcc_visibility_demo.items (id integer PRIMARY KEY,note text);
INSERT INTO mvcc_visibility_demo.items VALUES (1,'old');
- 在甲中通过读取建立可重复读快照。
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id;
- 在乙中更新原记录、插入第二条记录,并查看已经提交的结果。
UPDATE mvcc_visibility_demo.items SET note='new' WHERE id=1;
INSERT INTO mvcc_visibility_demo.items VALUES (2,'other');
SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id;
- 在甲中重读,插入属于自己的第三条记录,再读一次并提交。
SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id;
INSERT INTO mvcc_visibility_demo.items VALUES (3,'mine');
SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id;
COMMIT;
- 甲提交后使用新语句再次读取,随后在乙中清理。
SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id;
DROP SCHEMA mvcc_visibility_demo CASCADE;
PostgreSQL 18.6 实测中,甲第一次读取返回 (1,old)。乙不需要等待这个普通读者结束,就完成了更新和插入,并看到 (1,new) 与 (2,other)。甲再次读取时仍然只有 (1,old)。当甲插入自己的记录后,它能看到 (1,old) 和 (3,mine),但乙插入的第二行仍不属于甲的快照。这同时展示了他人提交的隔离与自身修改的可见性。
甲提交后,新语句能看到当前的三行:(1,new)、(2,other) 和 (3,mine)。因此,变化并不是某个旧版本在同一快照中忽然变新,而是读取边界已经改变。若只打开一个会话、不安排并发更新,就无法验证上述区别,也不能由一个静态查询证明 MVCC 在并发下的行为。
在用途结束时结束快照
需要这种读取契约时,才为相应工作选择事务级快照,并在操作完成后及时结束。调查长期快照时,应同时查看写入量与清理进度,而不是假设历史版本会立即自行消失。可重复的可见性也不会阻止所有跨行业务异常;某项操作仍可能需要可串行化隔离、显式协调或约束。MVCC 提供行版本可见性,完整的应用并发契约才决定基于这些记录作出的判断是否正确。
来源
- PostgreSQL 18:MVCC 介绍,行版本与读取并发,已核验 2026-09-10。
- PostgreSQL 18:事务隔离,快照寿命与自身写入可见性,已核验 2026-09-10。
- PostgreSQL 18:显式锁,读者与写者的锁冲突,已核验 2026-09-10。
- PostgreSQL 18:常规清理,保留可见版本并回收过期记录,已核验 2026-09-10。