五年前,有开发者在博客里吐槽 PostgreSQL 的 32 位事务号(XID)是块"狗皮膏药"------贴上去勉强能用,但总在关键时刻发作。五年过去,PG 社区至今没有合并原生的 64 位 XID 方案。而在 2026 年 8 月底发布的一个国产数据库新版本中,这个被诟病多年的老问题,第一次被做成了生产可用特性。
这篇文章分两条线:一条给入门者讲清楚"PG 的 XID 到底是个啥、为什么是隐患",另一条给内核开发者一份"它改了什么、没改什么、还差什么"的对照笔记。
一、PG 的 32 位 XID 为什么是"狗皮膏药"
PG 用 uint32 存事务号,一个集群最多同时存在约 43 亿个事务号。听起来海量,但高写入业务几天就能烧光------典型电商库一天就能跑出 5 到 10 亿事务。
XID 耗尽意味着什么?PG 的 MVCC 可见性依赖 t_xmin < current_xid 判断。号不够了就得循环复用,可怎么让"老号"和"新号"在 32 位空间里不撞车?PG 的解法是 frozen xid:把 XID 空间想象成一个圆,frozen xid 是圆上一点,顺时针是"已分配(过去)",逆时针是"可分配(未来)"。每消耗约 20 亿事务,frozen xid 就要挪一次------挪的方式是让 autovacuum 扫整张表,把足够老的行的 xmin 标记成 frozen(一个特殊位),被标记的行永久可见、不再参与可见性判断。
代价很沉重:
- 冻结风暴:全表扫描,IO 暴增、WAL 暴涨、主从延迟。
- 长事务逼停库:autovacuum 会跳过被事务占用的 page,长事务挂一天,freeze 就推迟一天。极端时剩余 slot 不足,PG 强制进入单用户模式,拒绝一切新事务,只为把全库 freeze 跑完。
- 运维心智负担:上线前得算清"一天多少事务、freeze 阈值配多少、SSD 扛不扛得住",算错就是生产事故。
PG 社区至今未改,理由务实:32 位 + freeze 能跑,改成 64 位要动 tuple header、CLOG、vacuum、复制协议、子事务、prepared transaction 等一大片代码,影响面太大。但 2026 年 8 月的新版本说明里明确写了:"支持 64 位事务 ID(XID),避免因 32 位事务 ID 回收不及时或长事务阻塞导致的业务中断。" 这是该系国产数据库首次把"64 位事务号"做成生产可用特性。
二、64 位 XID 改造的真实技术难点
理解这次改动的价值,先得理解 PG 为什么一直没动。它不是改个类型那么简单,牵动面极广:
- Tuple header 膨胀:xmin/xmax 改 64 位,每个 tuple 多 16 字节,10 亿行表仅 header 就多 16 GB------这是社区第一道犹豫。
- CLOG 存储瓶颈:原 SLRU 结构在 64 位下必须换(ring buffer 或 segment mmap),而动 CLOG 就动 vacuum 可见性。
- Vacuum 语义失效:32 位下"半个圆"的 freeze 逻辑,在 64 位下根本不需要------你一辈子写不到 2⁶³ 个事务。
- 子事务与 2PC :子事务、prepared transaction 都依赖 XID 命名空间,改宽后要重写
pg_subtrans、pg_prepared_xacts。 - 复制协议破裂:流复制协议里的事务号字段变宽,主从必须同时升级,跨版本复制会断------这是社区最忌讳的升级门槛。
三、这次到底改了什么(有取舍的实用方案)
读版本说明书可知,这次 64 位改造并非全栈改造,而是务实的渐进方案:
- 公开范围:支持 64 位 XID,解决 32 位回收不及时与长事务阻塞导致的业务中断。
- 实际落点:VACUUM 参数 int64 化。四个关键参数上限从约 20 亿级扩到 2⁶⁴-1:
| 参数 | 变更前 | 变更后 |
|---|---|---|
vacuum_freeze_min_age |
integer,最大 ~10 亿 | int64,最大 ~1.15×10¹⁹ |
vacuum_freeze_table_age |
integer,最大 20 亿 | int64,最大 ~1.15×10¹⁹ |
vacuum_multixact_freeze_min_age |
integer,最大 ~10 亿 | int64,最大 ~1.15×10¹⁹ |
vacuum_multixact_freeze_table_age |
integer,最大 20 亿 | int64,最大 ~1.15×10¹⁹ |
注意:默认值完全不变 (仍是 2 亿 / 4 亿 / 100 亿 / 200 亿量级),只是扩大了上限。这释放出一个关键信号------它大概率没有改 tuple header 里的 xmin 宽度,而是"扩展分配号空间 + 让 XID 不再循环",tuple 里仍保留 32 位、仍需要 freeze。
- 配套:实例级动态内存管控 。新增
max_dynamic_memory,全局限制实例内存,超限即告警,降低 64 位 XID 下事务生命周期变长、单次内存占用可能上升带来的 OOM 风险。 - 协同:FastPath 锁与 CSNLOG 分区。新增 FastPath 锁机制,并可配 CLOG、CSNLOG 轻量锁分区数------64 位 XID 必然伴随 CSN 调整,分区化是高并发的必要优化。
四、给入门者的 5 个心智模型
- "半圆"循环 :32 位下 XID 是个圆,frozen 点是分界,顺时针是过去、逆时针是未来。64 位下,这个圆变成直线------
t_xmin < 快照直接判可见,freeze 从"对抗循环"退化为"普通清理"。 - freeze 是"标记"不是"删除":它把 t_xmin 换成 FrozenTransactionId=2,让所有事务都视作"早于自己",从而释放字段循环复用。64 位下该机制仍在,但触发频率大幅下降。
- 实际收益:消除 freeze 风暴根源、弱化 autovacuum 的强约束、解除长事务逼停库、解耦从库延迟与 freeze。
- 不直接解决:表膨胀(仍需 vacuum)、长事务持有的锁(仍可能堵 DDL)。
- 调优哲学变了:32 位下这些参数是"硬卡窗口",64 位下变成"普通 cleanup 调优",可以更激进地配置,业务也不会立刻崩。
五、专家视角的实现笔记
- 混合方案 :本质是"分配层 64 位 + tuple header 仍 32 位",与社区讨论过的
xid8思路一致------xid8 用于系统表、快照、WAL,tuple header 保留 32 位(用 frozen bit 标识)。好处是不破 page 格式、不破 CLOG、升级平滑、运维心智兼容;代价是 tuple 仍受 32 位"半圆"约束,单表 43 亿事务上限不变。 - 过渡期调优 :迁移期保留 PG 经验值观察 freeze 频率;优化期可把
vacuum_freeze_min_age调小、autovacuum_freeze_max_age调大;平稳期再把vacuum_freeze_table_age放到很大,因为 freeze 已非高频操作。 - 跨版本对比 :PG 16 的
xid8只解决"用户能用 64 位字段",PG 17 修的是 multixact,PG 18 尚无全 64 位 XID 的 RFC。该版本是第一个把"全 64 位 XID 分配"做成生产可用的 PG 系国产库。 - 与其他库对比:Oracle 用 SCN、MySQL/InnoDB 用 undo,都不依赖 XID 循环;zheap/zedstore 是 64 位 TID 但仍是实验项目。金仓走的是"PG 兼容 + XID 空间扩展"的中间路径,与"不破生态、只扩关键节点"的路线一致。
六、对 PG 社区的三点启示
- 渐进式扩展值得抄作业:"分配层 64 位 + tuple header 32 位"的分层设计,能在不破坏 page 格式的前提下把事务号空间从 43 亿扩到 2⁶⁴,代价仅多一层"分配号 → tuple xmin"映射。
- 默认值保留是兼容性关键:升级参数类型时默认值与 PG 完全一致,这个不起眼的细节大幅降低了 DBA 迁移心智成本。
- freeze 价值被重新定义:从"对抗循环的紧急任务"回归"按业务需要清理死元组",对 autovacuum 调优哲学影响深远。
七、给应用开发者的建议
- 不要在应用层做事务号运算(事务号不连续,
xid+1无意义),64 位后这点依旧成立。 - freeze 不再是紧急任务,告警阈值可以放宽,给运维留更多空间。
- 跨版本升级测试更复杂,主从 XID 兼容性需要专门验证。
八、写在最后
五年前的那句吐槽,当时没人接。PG 社区认为 freeze + autovacuum 够用,国产库也没人敢碰这块。如今这个版本把 64 位 XID 做成了生产可用,完成了社区没完成的事。
它提醒我们:国产数据库不只在做兼容,也在啃内核里那些连上游都回避的硬骨头。这条"32 位 → 64 位 XID 的渐进改造"路径,值得后来者认真参考。
五年吐槽,一个版本号,问题被治好了。