在数据库系统里,多个会话同时修改和查询同一批数据是很常见的场景。PostgreSQL选择用多版本并发控制来管理这种行为,而不是让写事务长时间堵住读事务。核心思路是:每次更新都不是原地覆盖旧数据,而是产生一个新版本的元组,旧版本仍然保留,供正在运行的其他事务按它们启动时的快照去读取。这种做法直接塑造了PostgreSQL在并发下的隔离表现,也是它能在高并发Web服务中保持稳定的底层支撑。

MVCC的基本工作方式:行版本与事务ID
PostgreSQL的每一行数据在内部叫作一个元组,每个元组都带着两个关键字段:xmin和xmax。xmin记录创建这个版本的事务ID,xmax记录删除或更新掉这个版本的事务ID,如果还没被删改就设为空。事务在启动时会被分配一个递增的32位事务ID,系统据此判断哪个版本对当前事务可见。比如事务A插入一行,这行的xmin就是A的ID;事务B后来更新它,原行的xmax被标成B的ID,同时生成新行,新行xmin是B的ID。
可见性判断依赖事务快照和提交日志。快照里保存了当前事务看来哪些事务已经提交、哪些还在跑。提交日志位于pg_xact目录,记录每个事务的最终状态。当一个事务读数据时,会拿元组的xmin和xmax去比对快照与提交日志:只有xmin对应的事务已提交且不在快照的活跃列表里,并且xmax为空或对应事务未提交,这行才对你可见。下面的代码模拟了简化的可见性检查逻辑。
-- 查看表中元组的xmin与xmax,理解版本存在 SELECT xmin, xmax, ctid, username FROM account WHERE id = 1; -- 开启两个会话观察MVCC效果 -- 会话1 BEGIN; UPDATE account SET username = 'new_name' WHERE id = 1; -- 此时会话2查到的仍是旧版本,因为会话1未提交 -- 会话2 SELECT username FROM account WHERE id = 1; -- 返回旧值,MVCC保证非阻塞读
这种机制带来的直接好处是读写互不阻塞。读不用申请行级共享锁来防止别人改,写也不用等读结束。相比之下,用传统两阶段锁实现的数据库,读可能阻塞写、写必然阻塞读。MVCC把冲突化解成了多版本共存,代价是旧版本需要清理,这就引出了后续要谈的表膨胀问题。
事务快照与隔离级别如何借助MVCC实现
PostgreSQL提供读已提交和可重复读等主要隔离级别,它们都建立在MVCC之上,区别只在于快照的获取时机。读已提交级别下,每个语句执行前都会取一个新快照,所以同一事务里前后两条SELECT可能看到别的已提交事务的新结果。可重复读则在事务开始时取一次快照,整个事务期间都用它,从而看到一致的数据视图,避免不可重复读。
快照本身是一个结构体,包含最小活跃事务ID、最大已分配事务ID以及活跃事务位图。执行查询时,执行器把快照传给堆扫描,逐行做可见性函数判断。因为快照是轻量对象,取快照的成本很低,这也是PostgreSQL能支撑大量短事务的原因之一。下面的例子展示了在可重复读下快照隔离的效果。
-- 事务A使用可重复读 BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT balance FROM wallet WHERE uid = 10; -- 看到100 -- 事务B在别处提交修改 -- UPDATE wallet SET balance = 200 WHERE uid = 10; COMMIT; SELECT balance FROM wallet WHERE uid = 10; -- 事务A仍看到100 COMMIT;
需要注意,MVCC并不能靠快照解决所有并发异常。比如写偏斜在可重复读下仍可能发生,PostgreSQL用谓词锁或应用层加锁来补位。但绝大多数读多写少业务,仅依赖MVCC的快照读就能避开锁竞争,这也是大家感觉PostgreSQL并发顺滑的根本原因。理解快照生命周期,有助于我们控制长事务,因为长事务持有的快照会阻止旧版本被清理。
MVCC的代价与维护:表膨胀与VACUUM
因为更新和删除不立即回收空间,被淘汰的元组版本会留在页面里,形成死元组。如果这些死元组不被清理,表文件会持续变大,这称为表膨胀。VACUUM任务负责把对所有人不可见的死元组标记为空闲空间,供后续重用;VACUUM FULL则会重写表文件收缩尺寸,但要求排他锁,线上要谨慎。
长事务是MVCC的最大敌人之一。只要有一个事务长时间不提交,它持有的快照就让那个时间点之后的所有被更新版本都保留着,VACUUM无法移除它们。监控pg_stat_activity里的state和xact_start可以找出长事务。下面的查询能列出运行超过十分钟的事务。
SELECT pid, state, xact_start, now() - xact_start AS duration FROM pg_stat_activity WHERE state <> 'idle' AND xact_start < now() - interval '10 minutes' ORDER BY xact_start;
除了人工干预,PostgreSQL还有autovacuum后台进程,按表和死元组比例自动触发清理。调大autovacuum_vacuum_scale_factor或缩短autovacuum_naptime可加快回收,但会增加IO。对写入极频的表,常配合分区或定时手工VACUUM来平衡。理解MVCC原理后就会明白,所谓并发一致性不是免费午餐,而是用空间和多版本管理换来了读写自由,运维上必须重视版本回收。
MVCC与锁机制的协作边界
虽然MVCC解决了大部分读写冲突,但它不替代锁。表结构变更、显式行锁、唯一约束冲突检测仍要用到轻量级锁和表级锁。例如两个事务同时插入同一主键,后到的会被唯一索引的排他锁阻塞,这和MVCC无关。清楚边界才能正确排查:慢查询若是等Lock,不一定是MVCC的问题。
在显式加锁场景,SELECT FOR UPDATE会为选中的行打上事务标记,其他事务想更新同一行就得等。此时MVCC提供的是普通读不被阻塞,但被锁定的写意图仍需串行。了解这一点,设计高并发扣库存类逻辑时,可以把竞争行的修改尽量靠后,减少持锁时间,其余查询走MVCC快照,系统整体吞吐会更好。
BEGIN; SELECT * FROM stock WHERE sku = 'A' FOR UPDATE; -- 其他会话的FOR UPDATE或UPDATE会等待 UPDATE stock SET count = count - 1 WHERE sku = 'A'; COMMIT;
综上,PostgreSQL并发一致性依赖MVCC,是因为它用多版本和事务快照把读写冲突转为版本共存,再以VACUUM回收旧版本,辅以必要锁保证写串行。掌握这套核心原理,既能写好高性能SQL,也能在膨胀和锁等待出现时快速定位根因。
PostgreSQLMVCC并发一致性修改时间:2026-08-14 07:27:33