十、事务回卷与冻结
1. 事务概述
什么是数据库事务
- 单个逻辑工作单元执行的一系列操作
- 被BEGIN - COMMIT/ROLLBACK包裹的一组语句会被当作一个事务;
- 不显示指定BEGIN - COMMIT/ROLLBACK的单条语句也是一个事务;
- 对数据库进行读或写的一个操作序列
- 操作只有两种执行结果:提交或回滚
事务管理语句
- START TRANSACTION:此命令表示开始一个新的事务块
- BEGIN:初始化一个事务块,它和START TRANSACTION是一样的
- COMMIT:提交事务
- ROLLBACK:事务失败时执行回滚操作
- SET TRANSACTION:设置当前事务的特性,对后面的事务没有影响
- SAVEPOINT:大事务通过创建保存点把操作过程分成几个部分,来避免失败回滚整个事务
事务的ACID特性
| ACID特性 | 描述 |
|---|---|
| 原子性 (Atomicity) | 事务包含的所有数据库操作要么全部成功,要不全部失败回滚 |
| 一致性 (Consistency) | 事务完成后,数据库数据能够处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。 |
| 隔离性 (Isolation) | 一个事务未提交的业务结果是否对于其它事务可见。 |
| 持久性 (Durability) | 一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。 |
事务ID
- 每当事务开始时,事务管理器都会分配一个唯一标识符,称为事务 ID (txid)。PostgreSQL 的 txid 是一个 32 位无符号整数,大约 42 亿。
- 数据库中的事务 ID 递增,所以 XID 不是无限递增的整数,而是一个环。
- 如果在事务开始后执行内置的 txid_current() 函数,该函数将返回当前的 txid。
2. 事务回卷与冻结操作
核心结论: XID 回卷的本质不是事务 ID 数字重复,而是 PostgreSQL 失去判断 tuple 新旧关系的能力;事务冻结通过把足够老的 tuple 标记为「永远属于过去」,避免它们受到 XID 回卷影响。
XID 为什么会回卷
普通 XID 是 32 位无符号整数,大约 42 亿,达到最大值后会重新从低值继续使用。所以 XID 不是无限递增的整数,而是一个环:

PostgreSQL 如何比较两个 XID 的新旧
PostgreSQL 使用模 2^32 的环形比较规则,规定:
-
约 21 亿个 XID 被认为是「更旧」
-
约 21 亿个 XID 被认为是「更新」
临界值是 2^31 = 2,147,483,648。也就是说,一个 XID 最多只能安全地在环上和当前 XID 相隔约 21 亿个事务。
图中 100 和 2^31 + 100 表示什么
假设某个 tuple 的 xmin = 100:
-
当前事务接近 XID 100~200 时,
xmin = 100明显是过去创建的,tuple 可见。 -
但如果 PostgreSQL 又运行了超过约 21 亿个事务,当前 XID 到达环的另一侧,
xmin = 100可能会被误判为「未来的 XID」。
⚠️ 如果一个旧 tuple 一直存在而不处理,XID 回卷后它可能从「过去 → 未来」,导致旧数据突然不可见。
回卷不只是「ID 重复」
真正严重的问题是:PostgreSQL 失去判断 XID 新旧关系的能力。
例如:旧 tuple xmin = 100,当前事务已经运行超过 2^31 个事务。如果不处理,PostgreSQL 可能错误地认为 xmin = 100 是未来事务,于是本来应该可见的行被当作不可见。
这不是普通的数据覆盖,而是 MVCC 可见性语义被破坏。
什么是事务 ID 冻结
PostgreSQL 会把足够老的 tuple 标记为 frozen。冻结的语义是:
这个 tuple 的创建事务已经老到可以确定对所有现在和未来的事务都可见,因此不再需要按照普通 XID 规则比较。
冻结后的 tuple 被视为由特殊事务 ID FrozenTransactionId 创建,永远被认为早于所有普通事务,不会受到回卷影响。
|
版本 |
冻结方式 |
|---|---|
|
PostgreSQL 9.4 之前 |
直接把 tuple 的 xmin 改成 |
|
PostgreSQL 9.4 之后 |
在 tuple header 的 |
VACUUM 如何负责冻结
VACUUM 不仅清理死元组,也负责冻结旧 tuple。关键参数:
|
参数 |
默认值 |
作用 |
|---|---|---|
|
|
5000 万 |
tuple 的 XID 年龄超过此阈值时,VACUUM 可能将其冻结 |
|
|
1.5 亿 |
表的 relfrozenxid 年龄达到此值时,VACUUM 进行 aggressive vacuum |
|
|
2 亿 |
超过此阈值时强制启动防回卷 autovacuum |
普通 VACUUM 利用 Visibility Map 跳过已确认没有死 tuple 的页面,不一定冻结所有旧 tuple。只有 aggressive vacuum 才会扫描所有尚未标记为 all-frozen 的页面。
PostgreSQL 如何防止回卷
当某个表的 relfrozenxid 年龄超过 autovacuum_freeze_max_age(默认 2 亿)时,PostgreSQL 会强制启动防回卷 autovacuum。
⚠️ 即使设置
autovacuum = off,防止 XID 回卷的 autovacuum 仍然会启动。
回卷风险时会发生什么
如果 PostgreSQL 发现最老的 XID 接近回卷点,会先产生警告:
WARNING: database "mydb" must be vacuumed within N transactions
如果继续忽略,最终可能出现:
ERROR: database is not accepting commands that assign new transaction IDs
to avoid wraparound data loss
此时:
-
已经存在的事务可以继续
-
只读事务可能还能启动
-
新的写事务会被拒绝
-
必须尽快执行数据库级 VACUUM
如何查看数据库的 XID 年龄
查看每个数据库:
SELECT
datname,
age(datfrozenxid) AS xid_age
FROM pg_database
ORDER BY xid_age DESC;
查看表(含 TOAST):
SELECT
c.oid::regclass AS table_name,
greatest(
age(c.relfrozenxid),
age(t.relfrozenxid)
) AS xid_age
FROM pg_class AS c
LEFT JOIN pg_class AS t
ON c.reltoastrelid = t.oid
WHERE c.relkind IN ('r', 'm')
ORDER BY xid_age DESC;
查看当前 XID:
SELECT txid_current();
查看某一行的 xmin:
SELECT
xmin,
age(xmin),
ctid,
*
FROM public.test
LIMIT 10;
冻结进程

冻结进程将事务id为99和100元组冻结,并打上冻结标志。后面的事务就认为事务id为99和100为过去事务,所有事务都可见。如果后面的事务又用到事务id为99和100,那么新的元组里面没有冻结标志,就可以正常使用。
惰性冻结
惰性冻结扫描VM,当可见行位位1是就跳过该page
OldestXmin = 50,002,500
freezeLimit_txid = 2500 (= OldestXmin - vacuum_freeze_min_age)

急性冻结
扫描所有尚未标记为 all-frozen 的页面
OldestXmin = 150,002,000
freezeLimit_txid = 100,002,000 (= OldestXmin - vacuum_freeze_min_age)

浙公网安备 33010602011771号