PostgreSQL 事务 ID 年龄增长与冻结机制分析

PostgreSQL 事务 ID 年龄增长与冻结机制分析

核心结论

表 t1 在 freeze 之后,即使不修改 t1、只修改其他表,t1 的年龄(age)仍然会持续增长。

原因:表的年龄 = 当前全局 XID 计数器值 - 表的 relfrozenxidrelfrozenxid 是冻结时刻的快照值,冻结后不再变化;但全局 XID 计数器随所有事务(不管操作哪个表)持续推进,因此差值不断增大。


1. 什么是"表的年龄"

PostgreSQL 中"年龄"指的是事务 ID 的差距,核心公式:

复制代码
age = current_xid - relfrozenxid

其中:

  • current_xid:全局下一个待分配的事务 ID(ReadNextTransactionId()
  • relfrozenxid:表的冻结水位线,记录在该表中所有 < relfrozenxid 的事务 ID 已被替换为 FrozenTransactionId(2)

1.1 age() 函数实现

源文件src/backend/utils/adt/xid.c:97-111

c 复制代码
/*
 *		xid_age			- compute age of an XID (relative to latest stable xid)
 */
Datum
xid_age(PG_FUNCTION_ARGS)
{
	TransactionId xid = PG_GETARG_TRANSACTIONID(0);
	TransactionId now = GetStableLatestTransactionId();

	/* Permanent XIDs are always infinitely old */
	if (!TransactionIdIsNormal(xid))
		PG_RETURN_INT32(INT_MAX);

	PG_RETURN_INT32((int32) (now - xid));
}

关键点:now 通过 GetStableLatestTransactionId() 获取,是全局的当前事务 ID 参考点。

1.2 GetStableLatestTransactionId 实现

源文件src/backend/access/transam/xact.c:534-557

c 复制代码
/*
 * Get the transaction's XID if it has one, else read the next-to-be-assigned
 * XID.  Once we have a value, return that same value for the remainder of the
 * current transaction.
 */
TransactionId
GetStableLatestTransactionId(void)
{
	static LocalTransactionId lxid = InvalidLocalTransactionId;
	static TransactionId stablexid = InvalidTransactionId;

	if (lxid != MyProc->lxid)
	{
		lxid = MyProc->lxid;
		stablexid = GetTopTransactionIdIfAny();
		if (!TransactionIdIsValid(stablexid))
			stablexid = ReadNextTransactionId();
	}

	Assert(TransactionIdIsValid(stablexid));
	return stablexid;
}

逻辑:

  1. 如果当前事务已分配 XID,使用该 XID
  2. 否则读取全局下一个待分配 XID(ReadNextTransactionId()
  3. 同一事务内缓存结果,保证稳定性

1.3 ReadNextTransactionId 实现

源文件src/include/access/transam.h:309-313

c 复制代码
static inline TransactionId
ReadNextTransactionId(void)
{
	return XidFromFullTransactionId(ReadNextFullTransactionId());
}

这是从共享内存的 ShmemVariableCache->nextXid 中读取的全局计数器值。


2. relfrozenxid 的定义

源文件src/include/catalog/pg_class.h:125-126

c 复制代码
/* all Xids < this are frozen in this rel */
TransactionId relfrozenxid BKI_DEFAULT(3);	/* FirstNormalTransactionId */

语义:

  • 所有 XID < relfrozenxid 的元组已被冻结(xmin 替换为 FrozenTransactionId = 2
  • 默认值为 3(FirstNormalTransactionId
  • 每个表独立维护自己的 relfrozenxid

3. 年龄增长的根本原因

3.1 全局 XID 计数器是共享的

PostgreSQL 的全局事务 ID 计数器(nextXid)是集群级别的共享资源:

复制代码
┌─────────────────────────────────────────────────────────┐
│              全局 nextXid 计数器 (共享内存)                │
│                                                         │
│   所有数据库、所有表的事务共用同一个计数器                    │
│   每个写事务都会推进 nextXid +1                            │
└───────────────────────┬─────────────────────────────────┘
                        │
            ┌───────────┼───────────┐
            ▼           ▼           ▼
       ┌────────┐  ┌────────┐  ┌────────┐
       │ 表 t1  │  │ 表 t2  │  │ 表 t3  │
       │frozen  │  │ 活跃   │  │ 活跃   │
       │xid=500 │  │  修改  │  │  修改  │
       └────────┘  └────────┘  └────────┘
            │           │           │
            ▼           ▼           ▼
       relfrozenxid   新元组       新元组
       固定不变       消耗 XID     消耗 XID

3.2 时间线示例

复制代码
时刻 1:对表 t1 执行 VACUUM FREEZE
  ├─ 全局 nextXid = 1000
  ├─ t1.relfrozenxid 被设置为 1000
  └─ age(t1) = 1000 - 1000 = 0

时刻 2:对表 t2 执行了大量 INSERT(100 万次)
  ├─ 全局 nextXid 推进到 1,001,000
  ├─ t1.relfrozenxid 仍为 1000(未修改 t1)
  └─ age(t1) = 1,001,000 - 1000 = 1,000,000  ← 年龄增长了!

时刻 3:继续操作其他表,又消耗了 500 万个事务
  ├─ 全局 nextXid = 6,001,000
  ├─ t1.relfrozenxid 仍为 1000
  └─ age(t1) = 6,001,000 - 1000 = 6,000,000  ← 继续增长

3.3 为什么即使不修改 t1,年龄也增长?

因素 说明
全局 XID 计数器 所有写事务共享,操作任何表都会推进
relfrozenxid 冻结后固定不变,直到下次 freeze
age 计算公式 age = nextXid - relfrozenxid,分子增长,分母不变
结论 只要集群中有任何写事务发生,所有表的年龄都会增长

4. 数据库级别年龄

除了表级别,数据库也有 datfrozenxid

sql 复制代码
SELECT datname, age(datfrozenxid) FROM pg_database;

数据库的 datfrozenxid = 该数据库中所有表 relfrozenxid 的最小值。其增长逻辑与表级别一致。


5. 年龄增长的安全阈值

源文件src/include/access/transam.h:31-35

c 复制代码
#define InvalidTransactionId		((TransactionId) 0)
#define BootstrapTransactionId		((TransactionId) 1)
#define FrozenTransactionId			((TransactionId) 2)
#define FirstNormalTransactionId	((TransactionId) 3)
#define MaxTransactionId			((TransactionId) 0xFFFFFFFF)

XID 是 32 位无符号整数,最大值 2^32 - 1 ≈ 42 亿。年龄达到约 20 亿时会触发回卷(wraparound)保护:

参数 默认值 说明
autovacuum_freeze_max_age 2 亿 自动 vacuum 强制冻结阈值
vacuum_freeze_table_age 1.5 亿 VACUUM 时执行全表扫描冻结的阈值
vacuum_freeze_min_age 5000 万 被冻结元组的最小年龄

6. 总结

问题 答案
表 t1 freeze 后不修改,只操作其他表,t1 的年龄会涨吗? 会涨。全局 XID 计数器持续被推进,age = nextXid - relfrozenxid 差值不断增大
数据库年龄什么情况下增长? 只要集群中存在任何写事务,所有数据库和表的年龄都会增长
年龄增长的速率取决于什么? 取决于整个集群的事务吞吐量,与具体操作哪个表无关
如何控制年龄? autovacuum 会在达到 autovacuum_freeze_max_age 时自动触发 freeze

核心要点:年龄增长不是坏事,它是 PostgreSQL 正常的运行机制。关键是在年龄接近危险阈值(20 亿)之前完成 freeze,防止 XID 回卷导致数据丢失。

相关推荐
这个DBA有点耶14 小时前
2026年分布式数据库有哪些主流选择?先评估这5个维度再决定
数据库·分布式·dba
这个DBA有点耶17 小时前
从MySQL 5.7到8.0:JSON查询性能差在哪?虚拟列索引vs多值索引怎么选
数据库·mysql·架构
胡写代码17 小时前
数据库审计字段,别再每张表各写一套了——我统一成这 6 个字段
数据库
SelectDB21 小时前
为什么 JSON 正在成为分析数据库新的竞争点?
数据库·json·agent
ClouGence1 天前
开源数据库管理工具 CloudDM 4.2.0 发布,新增 GoldenDB、KingbaseES 等数据源
数据库·dba·devops
发霉的馒头1 天前
ORA-00845: MEMORY_TARGET not supported on this system的解决方法
数据库
草莓熊Lotso1 天前
【Redis 初阶】Set 类型深度解析:去重集合的运算能力与实战场景
linux·网络·数据库·windows·redis·tcp/ip·缓存
煎饼皮皮侠1 天前
【设计】设计一个web版的数据库管理平台后端(之五) --借鉴mybatis
数据库·mybatis
东风破_1 天前
danci 2:创建的单词书到底存在哪里?从 Supabase 一路理解 ORM、Drizzle 和 RLS
数据库·后端·node.js
橙子家1 天前
OSS 文件上传的几个风险点和解决方案
数据库