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

相关推荐
菜鸟上路_lbz1 分钟前
sqlserver存储过程查询缓慢锁表分析
数据库·sqlserver
Elastic 中国社区官方博客3 分钟前
在 Elasticsearch 中使用利润率与流行度加权来优化电商搜索
大数据·数据库·elasticsearch·搜索引擎·全文检索
van久31 分钟前
Day32:项目性能优化(EF Core + 分页 + 全异步)
数据库·oracle·性能优化
Dxy12393102161 小时前
Python请求方式介绍:JSON、表单及其他常见数据传输格式
数据库·python·json
环流_3 小时前
Redis中string类型的应用场景
数据库·redis·缓存
倔强的石头_3 小时前
拒绝被复杂报表拖垮!HTAP场景下“标量子查询消除”硬核调优指南
数据库
环流_3 小时前
redis中list类型
数据库·redis·list
jiayong233 小时前
Tool Permission 与 Sandbox 的安全流水线:Agent 工具系统的工程边界
java·数据库·安全·agent
weixin_444012934 小时前
如何在MongoDB中实现按时间跨度的分片路由_时间序列范围分片与冷热节点架构
jvm·数据库·python
六月雨滴4 小时前
块(Block)管理
数据库·oracle·dba