PostgreSQL 事务 ID 年龄增长与冻结机制分析
核心结论
表 t1 在 freeze 之后,即使不修改 t1、只修改其他表,t1 的年龄(age)仍然会持续增长。
原因:表的年龄 = 当前全局 XID 计数器值 - 表的 relfrozenxid。relfrozenxid 是冻结时刻的快照值,冻结后不再变化;但全局 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;
}
逻辑:
- 如果当前事务已分配 XID,使用该 XID
- 否则读取全局下一个待分配 XID(
ReadNextTransactionId()) - 同一事务内缓存结果,保证稳定性
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 回卷导致数据丢失。