那个盯了三十年的"事务号水位"
用惯了 PostgreSQL 系数据库的 DBA,心里大概都装着一件事:事务号会不会用完。
事务号(XID)在 MVCC 里决定一行数据对哪个事务可见——每行元组头里记着 xmin(谁插进来的)和 xmax(谁删掉的),可见性判断本质上就是拿当前 XID 和元组上的 XID 做一次模 2³² 的减法比较。
传统 32 位 XID 沿一个圆环从 0 数到 2³²−1(约 42.9 亿),再绕回 0。环上没有绝对原点,"过去 / 未来"只是相对当前 XID 而言的两个半圈。一旦某个元组的 xmin 被时间"甩"过了半圈,落到"未来半圈"那一侧,它就会被误判成"未提交事务"——已提交的数据凭空消失。这就是社区著名的 XID wraparound(事务号回卷)。
金仓 KES 在 V9R2C16 版本(2026 年 8 月发布)上把这件事从根上动了一刀:全面支持 64 位事务 ID。本文就把"改了什么、参数怎么变、实测对不对、余量还有多少、哪些活还得干"挨个核对一遍。
一、32 位 XID:悬在头顶的那把剑
| 风险场景 | 后果 |
|---|---|
| 长事务阻塞 | 长时间未提交的事务会把最老快照(oldest xmin)钉死,autovacuum 推不动 relfrozenxid,事务号年龄持续增长,逼近回卷阈值 |
| autovacuum 回收不及时 | 高并发写入下事务号消耗速度远超冻结速度,即便手动 VACUUM FREEZE 也可能来不及 |
在 32 位架构里,有三级"阶梯式防御":
vacuum_freezemin_age(默认 5000 万):任何一次 VACUUM,都会把比这个年龄更老的 XID 直接标记为冻结;vacuum_freezetable_age(默认 1.5 亿):表年龄到了这个值,下次 VACUUM 切换成"全表扫描 + freeze";autovacuum_freezemax_age(32 位下默认 2 亿):表年龄到了这个值,强制启动防回卷 autovacuum,即使autovacuum=off也会拉起。
真正危险的是再往上:年龄逼近 2³¹(约 21 亿) 时,数据库会进入保护状态,强制只读、拒绝所有写入——库没宕,业务先停了。
一笔直观的账:按平均每秒 1000 个事务估算,约 42.9 亿的事务号大约六周左右就会绕完一圈。
二、KES 64 位事务 ID 改了什么
KES 64 位事务 ID 把值域从约 43 亿直接抬到 约 2⁶⁴−1 ≈ 1.84×10¹⁹,可使用的最大事务 ID 约为 1152921504606846975。这意味着:
事务号单向递增,不再有环形回卷的可能,从底层空间上规避了 XID wraparound 导致的业务中断;
即便挂着长事务、挂着未清理的 2PC 预备事务,也不会因为"事务号被钉住"而触发强制只读;
监控告警阈值同步抬升:32 位下
age超过2³²−30,000,000就要人工介入;64 位下告警阈值提升到2⁶⁴−1量级,误报大幅降低。
同时,与年龄相关的返回值类型也跟着升级:age(datfrozenxid) 的返回类型由 integer 变为 bigint——老巡检脚本若还用 32 位整型变量接收,会溢出或截断。
三、VACUUM / Autovacuum 参数变化核对
| 参数 | 改造前(32 位) | 改造后(64 位) |
|---|---|---|
autovacuum_freeze_max_age | integer,默认 2 亿,最大 20 亿 | int64,默认 100 亿,上限约 2⁶⁰−1 |
autovacuum_multixact_freeze_max_age | integer,默认 4 亿,最大 20 亿 | int64,默认 200 亿,上限约 2⁶⁰−1 |
特别澄清一个常见混淆
体验官征文里常提到的"默认值由 4 亿提升至 200 亿",严格对应的是 autovacuum_multixact_freeze_max_age(多事务 ID 冻结阈值);而大家更常打交道的 autovacuum_freeze_max_age,变化是 2 亿 → 100 亿。两者极易记串:
autovacuum_freeze_maxage:针对普通事务 ID,管sysclass.relfrozenxid;autovacuum_multixact_freeze_max_age:针对多事务 ID(MultiXact),管sysclass.relminmxid——多事务主要来自SELECT ... FOR SHARE/UPDATE这类行锁场景。它同样"即使 autovacuum 被禁用,系统也会强制拉起自动清理来阻止回卷",清理后还能从sysmultixact/members、sys_multixact/offsets子目录移除旧文件。
两个提醒:①vacuum_freeze_table_age的实际有效上限是0.95 × autovacuum_freeze_max_age,高于此值会被截断,经验法则是让它明显低于autovacuum_freeze_max_age,给手动 VACUUM 留提前介入的窗口;② 这两个 freeze 阈值参数只能在服务器启动时设置、修改需重启,不能靠ALTER SYSTEM在线改。
四、实测记录
下面把升级到 64 位事务 ID 后可以亲手复现的验证动作逐项列出来。所有数值均来自实测记录,环境以 KES V9R2C16 系列(64 位 XID 版本)为准。
4.1 实测环境
| 项目 | 配置 |
|---|---|
| 数据库 | KingbaseES V9R2C16 系列(支持 64 位 XID) |
| 兼容模式 | PostgreSQL 兼容模式(下列查询均在此模式下核对) |
| 客户端 | ksql |
| 用途 | 参数默认值核对、返回类型核对、冻结功能验证 |
4.2 实测一:一条 SQL 看清 freeze 参数的默认值、类型与上限
SELECT name, setting, boot_val, vartype, max_val
FROM pg_settings
WHERE name IN ('vacuum_freeze_min_age',
'vacuum_freeze_table_age',
'autovacuum_freeze_max_age');

为什么先跑这条:pgsettings 的 bootval 是"没有额外配置时的启动默认值",vartype 反映数据类型,max_val 反映值域上限。三个字段一起看,就能判断这套实例到底是 32 位还是 64 位事务号架构——这是最省事的一次性判别方法。
对照记录(改造前 32 位环境的样例,仅作基线参考):
| 参数 | setting | boot_val | max_val |
|---|---|---|---|
autovacuum_freeze_max_age | 1500000000 | 200000000(2 亿) | 2000000000(20 亿) |
vacuum_freeze_min_age | 50000000 | 50000000(5000 万) | 1000000000 |
vacuum_freeze_table_age | 1400000000 | 150000000(1.5 亿) | 2000000000 |
可以看到 32 位环境下 autovacuum_freeze_maxage 的 bootval 是 2 亿、max_val 被压在 20 亿;升级到 64 位后,vartype 变为 bigint(int64),默认值与上限同步放开——这就是"参数跟着事务号值域一起升级"的直接证据。
4.3 实测二:核对 age() 的返回类型
实测结论:在 V9R2C16B006 上核对 age(datfrozenxid),返回类型为 bigint。


这是 64 位 XID 改造的必然结果:值域扩大后,年龄的计算结果必须跟着升级到 64 位整型。这一步实测的意义在于——它直接决定你现有巡检脚本的命运:
如果脚本里用 32 位整型(如
int)去接这个值,会溢出或截断;升级后必须把接收变量统一改成 64 位整型(bigint)。
建议做法:升级前先在一套测试实例上跑一遍巡检脚本,确认没有"类型溢出/截断"告警,再上生产。
4.4 实测三:核对 autovacuum_freeze_max_age 的默认值变化
32位:

64位:

实测结论:在 V9R2C16B006 上,autovacuum_freeze_maxage 的默认值从 2 亿变为 100 亿;autovacuum_multixact_freeze_max_age 从 4 亿变为 200 亿。
两条变化一起看,量级提升如下:
| 参数 | 32 位默认值 | 64 位默认值 | 提升倍数 |
|---|---|---|---|
autovacuum_freeze_max_age | 2 亿 | 100 亿 | 约 50 倍 |
autovacuum_multixact_freeze_max_age | 4 亿 | 200 亿 | 约 50 倍 |
这个"50 倍"很关键:它意味着在同样的业务写入速率下,防回卷 autovacuum 被强制触发的频率显著降低,大表被动全表冻结带来的 IO/WAL 抖动也随之被大幅推迟。
4.5 实测四:VACUUM FREEZE 在 64 位事务号下是否仍然正常工作
这是很多人心里的疑问:"改了 64 位,冻结机制还灵不灵?"
实测做法:对一张测试表执行 VACUUM FREEZE,然后回查该表的冻结水位:
SELECT relfrozenxid, age(relfrozenxid)
FROM sys_class
WHERE relname = 'tb1';

实测结论:执行 VACUUM FREEZE 后,sys_class 中该表的 relfrozenxid 更新为当前事务 ID(实测记录为 1162),age 变为 0,证明冻结功能在 64 位事务 ID 下完全正常。
这条实测很有价值:它说明 64 位 XID 并没有"移除"冻结机制,只是把触发阈值大幅推后。DBA 手动排 VACUUM FREEZE 的能力和语义都还在。
4.6 实测五:事务号消耗与类型边界

实测结论(事务号消耗行为):调用 txid_current() 会消耗一个事务号。实测记录中可以看到事务号单调递增的序列(示例:1164 → 1165 → 1166 → 1167),每次调用都会推进当前事务号。
这一点在 32 位时代是"双刃剑":既可以被用来主动推进事务号、加速触达 autovacuum 的年龄条件从而加快死元组回收;也意味着高频调用会加速事务号消耗。在 64 位下,消耗加速这件事基本不再构成风险。
实测结论(类型边界):KES 内部事务号类型 xid 为 32 位(int4),而对外导出函数 txid_current() 返回 64 位(bigint/int8)。这正是"内部仍是紧凑的 32 位存储、对外通过 64 位接口扩展"的设计体现——既避免了元组头大幅膨胀,又拿到了 64 位的事务号空间。
4.7 实测结论汇总
| 实测项 | 实测结果 | 对 DBA 的意义 |
|---|---|---|
| freeze 参数默认值/类型/上限 | vartype 变为 int64,默认值与上限同步放开 | 判定实例架构,确认升级生效 |
age() 返回类型 | bigint | 巡检脚本变量类型必须升级 |
autovacuum_freeze_max_age 默认值 | 2 亿 → 100 亿 | 强制冻结频率大幅下降 |
autovacuum_multixact_freezemax_age 默认值 | 4 亿 → 200 亿 | 多事务场景回卷压力同步缓解 |
VACUUM FREEZE 功能 | 正常,relfrozenxid 更新、age=0 | 冻结机制保留,可继续手工运维 |
| 事务号消耗 | txid_current() 每次消耗一个事务号,单调递增 | 事务号推进行为未变,风险已消除 |
五、长期运行余量:算一笔实账
参数变了,DBA 最关心的是:这套库到底能安心跑多久? 把 32 位和 64 位放在同一张表里比一比(按事务号总空间 ÷ 每秒事务数估算,未考虑冻结回收,属最保守口径):
| 每秒事务数 | 32 位(约 42.9 亿) | 64 位(约 1.84×10¹⁹) |
|---|---|---|
| 1,000 | 约 50 天 | 约 5.85 亿年 |
| 10,000 | 约 5 天 | 约 5,850 万年 |
| 100,000 | 约 12 小时 | 约 585 万年 |
| 1,000,000 | 约 1.2 小时 | 约 58.5 万年 |
上表为按值域上限做的理论估算示例,实际消耗取决于业务写入量、子事务数量与长事务情况,仅用于说明量级差异。
结论很直接:32 位时代"六周一轮回"的焦虑,在 64 位下变成了以"万年"为单位的事。 即使把写入压力放大到每秒百万级事务,理论余量仍有数十万年量级。
再看 freeze 触发点:64 位下 autovacuum_freeze_max_age 默认 100 亿,按每秒 1000 事务估算,第一次防回卷冻结大约在一百多天后才会被触发一次;而且因为值域是线性的、不存在回卷,这个触发只是"例行维护",不再是"救命操作"。
六、别误会:64 位 XID 不是"免运维"
64 位 XID 解决的是"事务号回卷"这一个先天硬伤,它没有改 MVCC 的核心逻辑,更不是运维万能药。 以下几件事,升级后照样得干:
表膨胀照样要治。 膨胀的根源是增删改产生的死元组堆积,和事务号是几位没有关系。VACUUM / Autovacuum 依旧是回收空间、控制膨胀的核心手段。
快照阻塞 VACUUM 的机制没变。 长时间运行的事务、
idle in transaction的空闲事务依旧会持有旧快照、阻塞全局死元组回收——这里要看的不是backendxid,而是backendxmin:backend_xmin年龄很大才真正阻塞 VACUUM。锁与 DDL 冲突问题没变。 长事务引发的行锁、表锁阻塞,DDL 失败、业务卡顿,由锁调度和隔离级别决定,本次内核升级没有改动锁相关逻辑。
freeze 机制只是"延后",没有"消失"。 KES 走的是渐进式升级:通过超大值域扩容把冻结触发时间推后到常规场景几乎无需人工干预;但在超长期运行、超大存量数据的极端场景下,freeze 仍有可能触发。
一句话总结:它把"必须救火"变成了"几乎不用管",但没有把"日常保养"这件事取消。
七、升级后,运维清单要动的几处
| 项目 | 动作 |
|---|---|
| 巡检脚本变量类型 | 接收 age() 结果的变量统一改为 bigint,防止溢出/截断(见 4.3 实测) |
| 告警阈值 | 按 64 位量级重新设定,撤掉 32 位时代的"接近 21 亿就告警"规则 |
| 参数核对 | 用 4.2 的只读 SQL 确认 autovacuum_freeze_max_age、autovacuum_multixact_freeze_max_age 的实际值与类型 |
| 冻结功能验证 | 用 4.5 的只读 SQL 确认 VACUUM FREEZE 后 relfrozenxid 更新、age=0 |
| 仍要保留的监控 | 长事务、idle in transaction、表膨胀、死元组、autovacuum 执行情况——一个都不能撤 |
| 启动参数调整 | 涉及 freeze 阈值的调整需在启动时传入并重启生效,不能在线改 |
小结
从 43 亿到 1.84×10¹⁹,KES 64 位事务 ID 做的不是一次"参数微调",而是把 PostgreSQL 系数据库传承了三十年的环形事务号架构换成了一条几乎看不到尽头的线性通道。
真正根治的:事务号回卷、强制只读、大表被动全表冻结带来的业务抖动;
实测验证的:
autovacuum_freeze_max_age(2 亿→100 亿)、autovacuum_multixact_freeze_max_age(4 亿→200 亿)、age()返回值类型(integer→bigint)、VACUUM FREEZE仍正常工作;依旧要做的:膨胀治理、长事务治理、锁冲突优化——VACUUM / Autovacuum 依然是必修课。
对一线 DBA 来说,最大的感受或许是:盯了三十年的事务号水位表,终于可以从"每天看"降级成"偶尔看一眼"了。




