PostgreSQL 中的事务 ID 回卷和“冻结“
事务 ID 回卷Transaction ID Wraparound 是 PostgreSQL 中最严重、最致命的潜在故障之一。如果处理不当它会导致数据库停止服务甚至造成数据永久丢失。简单来说这是因为 PostgreSQL 用来标记事务的“计数器”用完了不得不从头开始数从而导致新旧数据混淆。核心原理32位整数的限制PostgreSQL 使用一个 32位无符号整数 (xid) 来标识每一个事务。最大值 2^32-1≈42.9亿。含义PG 最多只能容纳约 42 亿个事务。为什么回卷会导致数据丢失PostgreSQL 依靠 MVCC多版本并发控制 来判断哪行数据对当前事务可见。判断逻辑依赖于比较 当前事务 ID 和 数据行的创建/删除事务 ID。正常情况当前事务 ID 100。数据行 A 由事务 50 创建。因为 10050所以事务 100 能看到 数据行 A。回卷发生时灾难场景假设事务 ID 已经跑到了 42 亿然后回卷到了 10。旧数据由事务 40 亿 创建在物理时间上是很久以前创建的逻辑上应该是“过去”。当前事务ID 为 10。比较结果PG 发现 1040亿。错误判断PG 会认为事务 40 亿是未来发生的事务后果根据 MVCC 规则当前事务不能看到“未来”的数据。于是所有旧数据突然对查询不可见了 你的表看起来像是空了或者数据错乱。PostgreSQL 如何防止回卷Vacuum Freeze为了防止这种灾难PostgreSQL 引入了一种机制叫 “冻结”Freezing。什么是冻结当VACUUM进程运行时它不仅清理死元组还会检查那些非常古老的活元组。如果某个数据行的事务 ID 已经很老比如超过 20 亿VACUUM 会将该行的事务 ID 替换为一个特殊的常量FrozenXID通常设为 2。含义FrozenXID代表“这个数据在所有事务之前就已经存在了”。效果无论当前事务 ID 回卷到多少FrozenXID(2) 永远小于任何正常事务 ID。因此这些数据永远可见不会再参与大小比较。关键阈值PG 设定了一个安全界限通常是 20 亿个事务autovacuum_freeze_max_age默认 2亿但内部保护机制在 20亿左右强制介入。如果一个表中有数据行的年龄超过 20 亿事务未被冻结PG 会发出警告。如果超过 21.47 亿2^31PG 会强制停止所有写操作并强制执行紧急 Vacuum Freeze直到所有旧数据被冻结。此时数据库处于只读模式业务完全中断。如何监控事务 ID 年龄你可以查询pg_stat_user_tables或pg_class来查看表的“年龄”Age。SELECT relname AS table_name, age(relfrozenxid) AS transaction_age, -- 事务年龄 mxid_age(relminmxid) AS multixact_age -- 多事务年龄 FROM pg_class WHERE relkind r -- 普通表 ORDER BY age(relfrozenxid) DESC LIMIT 10;age含义当前最新事务 ID 与该表最老未冻结事务 ID 的差值。危险信号age 10亿需要关注检查 Autovacuum 是否正常工作。age 15亿高危建议手动执行VACUUM FREEZE。age 20亿紧急 数据库可能即将进入强制只读保护状态。为什么会发生回卷危机通常是因为 VACUUM 没有及时运行导致旧数据没有被冻结。常见原因包括Autovacuum 被禁用或配置错误有人为了性能关掉了自动真空。长事务阻塞如你之前遇到的长事务阻止了 VACUUM 清理和冻结旧数据。高并发写入事务产生速度极快Autovacuum 跟不上节奏。复制槽滞后逻辑复制槽阻止了 WAL 日志的清理进而影响 Vacuum。解决方案与预防预防措施保持 Autovacuum 开启这是最重要的防线。调整冻结参数对于高频更新的大表降低冻结阈值ALTER TABLE my_table SET (autovacuum_freeze_max_age 50000000); -- 5千万避免长事务长事务会阻止 Vacuum 冻结旧数据。监控 Age设置监控告警当age(relfrozenxid)超过 10 亿时报警。紧急处理如果 Age 接近 20 亿手动执行 Vacuum FreezeVACUUM FREEZE VERBOSE my_table;这会强制冻结所有旧数据降低表的 Age。注意这会消耗大量 IO 和 CPU可能在业务低峰期执行。如果数据库已进入只读保护你必须等待强制 Vacuum 完成。这个过程可能非常漫长几小时甚至几天取决于表大小。在此期间无法写入任何数据。