PostgreSQL的xmin与xmax系统列如何影响并发控制?

来源:IOS教程作者:陈远山头衔:网络博主
导读:本期聚焦于陈远山创作的《PostgreSQL的xmin与xmax系统列如何影响并发控制?》,敬请观看详情。xmin和xmax并不是表里真实存在的业务字段,而是PostgreSQL中每个元组头部自带的事务标记。很多初学者以为MVCC仅依赖快照机制,其实这两个系统列才是判断行可见性的核心依据。插入一行时,xmin会记录插入事务的ID;删除一行时,xmax会记下删除事务的ID;UPDATE操作则会让旧行的xmax和新行的xmin同时发生变化。借助这两个系统列,我们可以直接在应用层实现乐观锁,也能观察行版本的历史脉络。本文将结合具体SQL演示xmin和xmax的变化规律,分析在使用它们过程中容易踩到的坑,并给出基于xmin实现并发控制的完整示例。读完你会对PostgreSQL的行级并发管理有更清晰的认识。

PostgreSQL的每张普通表中,除了开发者自己声明的字段之外,还隐藏着一批系统列。其中xmin与xmax直接参与MVCC可见性判断,却容易被当成普通字段误用。了解这两个系统列,能帮助我们理解UPDATE的真实执行代价、行锁的生效时机,也能在应用层用极少代码实现乐观并发控制。

PostgreSQL的xmin与xmax系统列如何影响并发控制?

xmin和xmax到底是什么

先看底层存储。PostgreSQL的堆文件中,每个行版本都拥有一个行头,行头里记录了该版本的事务信息。xmin记录的是插入这个行版本的事务ID,xmax记录的是删除或锁定这个行版本的事务ID。如果xmax为0,表示这个行版本没有被删除,也没有被任何事务锁定。

这两个系统列不需要建表时声明,也不占用业务字段的存储空间。你可以像查询普通列一样把它们读出来,通过这个办法快速验证一个行版本是什么时候修改的。下面这段建表和查询语句展示了最基本的用法:

CREATE TABLE product (
    id   integer PRIMARY KEY,
    name text,
    price numeric
);

INSERT INTO product VALUES (1, '示例商品', 100);

SELECT xmin, xmax, id, name, price FROM product;

执行上面的SELECT后,你会看到xmin是一个很长的整型数字,而xmax通常是0。这个数字就是插入操作所属事务的事务ID。即便没有显式开启事务,PostgreSQL也会为单条INSERT分配一个事务ID,并把语句执行和提交放在同一个隐式事务里。

有一点需要特别说明:xmin和xmax的值只有事务ID,并不包含提交时间。判断一个行版本是否对当前会话可见,PostgreSQL还需要把这些事务ID和当前SNAPSHOT中的事务状态表做对比。所以“xmin已经提交,且xmax未提交”才是行版本可见性的标准条件。

INSERT、UPDATE、DELETE时系统列如何变化

不同的数据操作会让xmin和xmax产生完全不同的变化。INSERT发生时会生成一个新的行版本,xmin被设置为当前事务ID,xmax保持为0。DELETE会将目标行版本的xmax设置为删除事务的事务ID,之后这个行版本就不再对任何新事务可见。

UPDATE比较特别,它实际是DELETE加INSERT的组合。执行UPDATE时,原行版本的xmax会被设置为当前事务ID,表示这个旧版本已经失效;同时系统会生成一个全新的行版本,新行版本的xmin也是当前事务ID。下面这个例子用事务ID函数展示了UPDATE后的现象:

BEGIN;

SELECT pg_current_xact_id() AS current_txid;

SELECT id, xmin, xmax FROM product WHERE id = 1;

UPDATE product SET price = 120 WHERE id = 1;

SELECT id, xmin, xmax, price FROM product WHERE id = 1;

COMMIT;

你在结果中会看到,第一次查询时xmin是一个旧值,xmax是0;UPDATE完成以后,xmin变成了与current_txid相同的数字。这说明新版本确实由当前事务创建。旧版本已经无法通过普通SELECT看到,因为它的xmax被标成了当前事务ID,而当前事务已经提交,所以旧版本对后续事务都不可见。

DELETE也可以用RETURNING来观察两个系统列。把被删除行的xmin和xmax同时返回,能够直观看到删除事务ID写入了xmax:

BEGIN;

INSERT INTO product VALUES (2, '测试商品', 50);

DELETE FROM product WHERE id = 2
RETURNING id, xmin::text AS insert_xid, xmax::text AS delete_xid;

COMMIT;

在这个结果集里,insert_xid是INSERT所属事务的ID,delete_xid是DELETE所属事务的ID。CREATE TABLE或者ALTER TABLE这类DDL操作本身也会产生事务ID,但通常不会直接影响普通行的xmin。

用xmin实现乐观锁

乐观锁常见的方法是维护一个version字段,每次更新时让version加一,然后通过WHERE version = ?来判断并发冲突。这套方案完全可以用xmin替代,因为每次UPDATE或DELETE都会改变行版本对应的事务ID。对应用来说,xmin就是一个天然的行版本号。

具体做法很简单:第一次读取数据时把xmin当作版本号保存下来;提交更新时,在UPDATE的WHERE条件中加入这个版本号。如果其他事务在两次操作之间修改过这行数据,当前事务更新到的行版本已经不是当初读到的那个,xmin条件必然不成立,UPDATE返回0行,应用可以据此提示用户重新加载数据。

-- 应用第一次读取,cur_version是查询得到的xmin值
SELECT xmin AS cur_version, id, name, price
FROM product
WHERE id = 1;

-- 用户填写修改后提交更新
UPDATE product
SET price = 130
WHERE id = 1
  AND xmin = 12345;  -- 替换成第一步查到的cur_version

-- 如果 UPDATE 返回 0 行,说明读取之后已经有其他事务修改了这行

这种做法的最大优点是不用额外增加version列,也不影响实体映射代码。但它有一个明显的限制:xmin并不是一个长期稳定的版本号。PostgreSQL的事务ID是32位整数,当系统累计达到约42亿个事务后,事务ID会回卷。为了防止回卷导致数据损坏,autovacuum会把足够老的行标记为冻结状态,被冻结行的xmin会变成特殊值2。假如应用长期使用xmin做乐观锁,一旦行被冻结,再做条件更新就可能失效。

因此,xmin这种乐观锁比较适合短期更新的内部系统。对于需要长期保存、跨版本迁移的核心业务数据,更推荐使用显式的版本字段。你可以在应用中把更新成功返回的行数和业务逻辑结合起来,在UPDATE后检查结果行数,实现同样的并发控制效果。

使用xmin和xmax的常见误区

误区一:认为xmax非零就代表数据已经删除。实际上,xmax除了标记删除,还承担行锁的功能。当某个事务执行SELECT FOR UPDATE或者SELECT FOR SHARE锁定了某一行时,这行的xmax会被设置为锁定事务的事务ID,但业务数据并没有被删除。其他事务看到xmax非零后,会根据锁模式决定是继续等待还是报出锁冲突。所以单独看xmax无法区分行是被删除还是被锁定,必须参考事务提交状态。

误区二:把xmin当作可跨数据库比较的纯数值。事务ID在每个数据库集群中是全局递增的,但不同数据库实例、不同集群之间没有可比性。此外,事务ID和当前快照的xmin边界有关,把另一个连接里看到的值拿过来直接放进SQL,很容易得到错误判断。

误区三:尝试给系统列建立普通索引来加速查询。PostgreSQL不允许把xmin、xmax这类系统列直接放入普通索引的索引键中。即使某些版本允许以表达式索引的方式规避限制,也会破坏系统列原本的语义,让维护者误以为它只是一个普通数值。正确的性能优化方向是利用可见性映射和VACUUM机制减少死元组扫描,而不是依赖系统列索引。

最后还要提醒一下,当前会话在快照未开启时,xmax的值有可能是0,也有可能是某个尚未提交的事务ID。如果你在应用代码里查询xmax做决策,建议把事务隔离级别设置为READ COMMITTED以上,并确保查询和更新处于同一事务中。真正严谨的并发控制,永远是从业务语义出发选择合适的锁策略,xmin和xmax只是辅助我们理解数据库内部行为的重要参考。

xminxmaxMVCC修改时间:2026-08-20 14:00:52

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