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 回卷导致数据丢失。

相关推荐
Blossom i22 分钟前
大数据预处理与采集实验一:使用Python操作MySQL数据库
数据库·mysql
雾时之林1 小时前
Linux--软件管理、源码包安装
linux·服务器·数据库
老纪的技术唠嗑局3 小时前
超级干货分享:中小企业使用 OceanBase 实践经验汇总
数据库
CLOUD ACE3 小时前
谷歌云代理|零售商Target 如何利用 Spanner Graph 提升零售发现体验并将数据库维护成本降低 50%
数据库·零售
AI大模型-小雄3 小时前
Codex写分页接口为什么越翻越慢?用Cursor Pagination解决重复与漏数据
大数据·数据库·elasticsearch·搜索引擎·chatgpt·后端开发·codex
少晓年4 小时前
从 MySQL 迁移到人大金仓 KingbaseES:完整指南与实践
数据库·mysql
SelectDB4 小时前
Apache Doris AI RAG 实战:从基础 RAG 到知识图谱增强的技术能力与选型
数据库
SelectDB5 小时前
电商企业 PostgreSQL 迁移 Apache Doris:80TB 分析数据统一平台技术能力与实践
数据库
SelectDB5 小时前
阶跃星辰 Agent 可观测:Apache Doris / SelectDB 的技术能力与实践
数据库
SelectDB5 小时前
ApacheDoris Iceberg V3 湖仓 DML:Apache Doris / SelectDB 的技术能力与实践
数据库