十、事务回卷与冻结

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 不是无限递增的整数,而是一个

image

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 改成 FrozenTransactionId = 2

PostgreSQL 9.4 之后

在 tuple header 的 t_infomask 中设置 HEAP_XMIN_FROZEN 标志,保留原始 xmin

 

VACUUM 如何负责冻结

VACUUM 不仅清理死元组,也负责冻结旧 tuple。关键参数:

参数

默认值

作用

vacuum_freeze_min_age

5000 万

tuple 的 XID 年龄超过此阈值时,VACUUM 可能将其冻结

vacuum_freeze_table_age

1.5 亿

表的 relfrozenxid 年龄达到此值时,VACUUM 进行 aggressive vacuum

autovacuum_freeze_max_age

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;

 

 

冻结进程

image

冻结进程将事务id为99和100元组冻结,并打上冻结标志。后面的事务就认为事务id为99和100为过去事务,所有事务都可见。如果后面的事务又用到事务id为99和100,那么新的元组里面没有冻结标志,就可以正常使用。

 

惰性冻结

惰性冻结扫描VM,当可见行位位1是就跳过该page

OldestXmin = 50,002,500
freezeLimit_txid = 2500 (= OldestXmin - vacuum_freeze_min_age)

image

 急性冻结

扫描所有尚未标记为 all-frozen 的页面

OldestXmin = 150,002,000
freezeLimit_txid = 100,002,000 (= OldestXmin - vacuum_freeze_min_age)

image

 

 

 

posted @ 2026-09-04 14:42  BinBin-HF  阅读(4)  评论(0)    收藏  举报