PostgreSQL 使用 32 位无符号整数表示事务 ID。对于负载较大的系统,以每秒 100 个事务的速度在连续运行大约一年多后就会耗尽 32 位的事务 ID。
为事务 ID 分配 64 位本可以彻底解决这个问题,但是将会被存储在元组头等多个地方,更大的数据类型会占用更多的空间。元组头目前已经相当大了(至少 24 字节),增加更多位将会再增加 8 字节。
为此,PostgreSQL 允许事务 ID 发生整数回绕(Wrapping),即 。在这种情况下,PostgreSQL 额外定义了事务 ID 的比较方式,如果两个 xid 之间的差值大于 ,则认为事务 ID 发生过整数回绕,其中 xid 较小的那一个才是更新的事务。在实现中,可以通过 (int)((unsigned int)xid1 - (unsigned int)xid2) < 0 来判断 xid1 是否早于 xid2。
但是当数据库中真的存在如此历史悠久的事务时,例如某个事务在经历 个事务后仍未完成,或者更常见的,某个元组被创建并经历 个事务后仍然未被修改或删除,此时事务 ID 将会被上面的算法错误地判断新旧关系。
为此,PostgreSQL 引入了冻结的概念,将与最新事务 ID 差值超过一定阈值的事务冻结,使其事务 ID 在与其他正常的事务 ID 比较时始终更旧。在 PostgreSQL 9.4 之前,这通过将事务的 ID 修改为一个固定的特殊值 2 实现(正常的事务 ID 均大于 2)。在 PostgreSQL 9.4 之后,这通过将元组的 xmin_committed、xmin_aborted 同时设置为 true 标记 xmin 事务已经被冻结。元组的 xmax 事务同理。
PostgreSQL 的冻结分为 lazy 和 aggressive 两种模式。
lazy 模式是伴随清理的扫描过程对老于 oldest_active_xmin - vacuum_freeze_min_age 的所有元组 xmin、xmax 事务进行冻结。这个过程中可能会因为可见性映射等跳过部分页面的标记,因此不会冻结所有满足条件的事务。
当 oldest_active_xmin - newest_frozen_xid > vacuum_freeze_table_age 时,会启动 aggressive 模式,忽略可见性映射而扫描整个表。
与清理过程类似,可见性映射为页面定义了 all_frozen 位,当页面中的所有元组都已被冻结时可以直接忽略对页面的扫描。
需要注意的是,PostgreSQL 只会冻结比最早的活跃事务 ID (oldest_active_xmin)更早的事务,因此冻结只会让这些已完成事务发生的先后顺序无法区分,并不会影响活跃事务自己对这些已完成事务以及之后其他事务顺序的判断。
如果数据库中真的存在一个一直未完成的事务且与最新事务的差值即将达到 vacuum_failsafe_age,那么 PostgreSQL 会进入 wraparound_failsafe 终止服务,拒绝一切操作直到管理员手动提交或回滚这个事务,避免数据不一致。
ERROR: database is not accepting commands to avoid wraparound data loss in database "template1"
HINT: Stop the postmaster and use a standalone backend to vacuum that database.
You might also need to commit or roll back old prepared transactions. CONTEXT: SQL statement "UPDATE XXX SET min_horizon = buffer_ts WHERE peer = peer_id"
PL/pgSQL function kv.XXX(regclass,integer) line 21 at SQL statement