导读:本期聚焦于小伙伴创作的《PostgreSQL并发一致性为何依赖MVCC?PostgreSQL MVCC核心原理是什么?》,敬请观看详情。事务并发读写同一张表时,为什么PostgreSQL很少出现锁等待和脏读?答案藏在多版本并发控制里。MVCC为每个事务提供数据的快照视图,写操作不阻塞读,读操作也不阻塞写。本文从行版本标记、事务ID分配、可见性判断规则等角度,说明PostgreSQL如何用xmin和xmax跟踪元组变更,以及提交日志与事务快照如何配合。理解这些机制能帮我们解释长事务导致表膨胀的原因,也能在排查锁问题时分清MVCC与表锁的边界。

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

PostgreSQL并发一致性为何依赖MVCC?PostgreSQL MVCC核心原理是什么?

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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。