PostgreSQL/循环和冻结
事务由标识符识别,这些标识符被实现为无符号的 32 位整数,称为 XID。事务及其 XID 在集群级别是已知的,涵盖所有数据库。类似于序列,XID 会随着每个新事务而增加 。迟早,这个有限的 数字空间将被耗尽,因此有必要从头开始重新启动序列(值 0、1 和 2 被保留用于特定目的)。这个 XID 的重启被称为 *循环*,每个循环被称为一个*纪元*。
不太可能在集群中同时存在超过 个事务,或者单个事务持续时间很长,以至于其 XID 与下一个循环的相同值发生冲突。乍一看,这种对“ 宇宙”的循环使用似乎是安全的,易于实现的。然而,这种简单的策略会导致巨大的问题。原因是 XID 存储在每行内的系统列中(请参见 MVCC 章节中的 xmin, xmax
)。并且行会长期停留在数据库中,在许多情况下会永远存在。
第一个问题是,循环后,下一个 XID(3、4、5,…)可能会与上一个纪元的 XID 发生冲突。它们不再是唯一的,因为旧行的系统列可能包含相同的值。但事务必须能够确定检索到的行是在其自身开始时间后还是很久以前被其他事务修改的。我们将第一个问题称为 **XID 冲突**。
第二个问题与 MVCC 和事务时间线相关。行可能存在于多个版本中。当事务修改行并保持更长时间存活时——由于更多活动而没有 COMMIT 或 ROLLBACK——其他进程应该“看到”该行的版本为事务开始时间时的版本,而不是未提交的修改。因此,机制必须隐藏正在进行的更改,让其他事务感觉数据环境是稳定的。系统通过在每个 SQL 命令(尤其是,但不仅仅是)系统列 xmin
中考虑其他标准来实现这一点。
您可以想象,这些额外的标准可以让系统在每个查询中无声地添加谓词 xmin < my_xid
。(这仅仅是伪代码中的说明,实际实现方式不同。)它保证了在请求事务开始后发生的更改对该事务是不可见的。
到目前为止,一切都很好。
但循环后会发生什么?下一个事务将拥有非常小的 XID,例如“5”。而 xmin < 5
的结果将是什么?几乎什么都没有。所有 xmin
介于 和 之间的行将不再是任何查询的一部分。与片刻之前的情况相反,所有数据突然消失了。它仍然存在于数据库中,但无法访问。我们将第二个问题称为 **突然死亡**。
步骤 1:在概念层面上,完整的 ' 宇宙' 被划分为两个 个数字的半部分。一个分割点是当前事务 ID pg_current_xact_id
(在 PostgreSQL 版本 13 之前称为 txid_current
),另一个是圆圈的另一端 pg_current_xact_id +
(或 pg_current_xact_id -
,它们是相同的)。因此,分割点不是固定值,而是动态地跟随新事务的进行而变化。一半代表以前使用过并因此用尽的 XID;另一半代表定义上是自由的 XID。它们将在未来分配。请注意动态方面:随着集群中每个新事务的出现,pg_current_xact_id
和 '过去 / 未来' 之间的边界都向前移动。这是一种穿越时间的无休止的行走比喻,在一段时间后,旧问题将被遗忘,或者至少被理想化。
该想法可以通过修改上述 xmin < my_xid
谓词为 if/else
块来实现。
if (my_xid < ) return rows with: xmin < my_xid OR xmin > my_xid + else return rows with: xmin < my_xid AND xmin > my_xid +
当然,这是一种简化,必须考虑许多其他标准,例如 COMMIT 状态、'已删除' 以及其他因素。它纯粹关注 '过去 / 未来' 比喻的方面。
注意:使用此算法,'关键点' 从 或者
变为
pg_current_xact_id +
。它被称为回绕点,而 pg_current_xact_id +
和 pg_current_xact_id
之间的线称为回绕视界。
步骤 2:概述的算法确保了所有可能 XID 的 50% 可见。但是其他 XID 怎么样呢?如上所述,行可能会永远保留在数据库中,在 xmin
中保存非常旧的 XID。这部分也必须考虑。访问所有可能的 XID 范围的想法是,用一个标志来补充以前的算法,该标志将某些行标记为 '永远可见'(或者直到下一次写入操作才可见)。只要有一个或多个事务可能获得对它们的写访问权限,这种标记就不可能实现。幸运的是,新 XID 的序列严格向前移动,并且随着时间的推移,具有旧 XID 的事务会结束。PostgreSQL 不仅知道当前 XID pg_current_xact_id
,而且还知道每个连接的最旧活动 XID(pg_stat_activity.backend_xmin),以及每个表(pg_class.relfrozenxid)和每个数据库(pg_database.datfrozenxid)的所有未冻结 XID 的下限。xmin
比 oldest(pg_stat_activity.backend_xmin)
旧的行是此类标志的候选。没有运行的事务拥有或将获得对它们的写访问权限,只有更新的事务可以。根据MVCC,它们将创建一个新的行版本,这个版本将保留原样。
它是 VACUUM 执行的两项主要职责之一,即执行冻结。它在这些已识别行的头部的 t_infomask
中用一个标志标记它们为 '永远可见'。从这一点起,不再进行与 xmin
的比较。这些行始终被视为可见,即使它们是 '未来' 的一部分。这种标记称为 FREEZE,行状态称为 FROZEN。
现在,用于检索行的算法更改为
-- 'frozen' rows will always be returned if (my_xid < ) return rows with: frozen OR (xmin < my_xid OR xmin > my_xid + ) else return rows with: frozen OR (xmin < my_xid AND xmin > my_xid + )
通过此扩展,解决了上述两个问题。系统即使在回绕后也能生成 XID,而不会有与旧 XID 冲突的风险。旧的 XID 可能会存在,但它们不会以任何方式被触碰。其次,该算法可以找到所有相关的 XID,无论是否发生了回绕。
回绕失败
[edit | edit source]一个事务可能有意或由于应用程序中的错误而长时间保持活动状态。随着时间的推移,它的 XID 成为整个集群中最旧的 XID,可以从 pg_stat_activity.backend_xmin 中检索到。只要这种情况持续存在,回绕点 pg_current_xact_id +
和 pg_stat_activity.backend_xmin 之间的差距越来越小。如果差距完全消失,我们将看到本章开头描述的所有问题。这称为回绕失败,必须在任何情况下都避免这种情况。VACUUM 尽其所能冻结尽可能多的行。但是,如果长时间运行的事务阻止冻结,并且差距的大小降至某个限制以下,VACUUM 将以 '激进模式' 运行,并作用于受影响表的全部页面,而与上述值无关;如果这也失败,集群将停止创建新事务并阻止进一步的写操作。
详细信息
[edit | edit source]注意:初学者可以跳过这些细节,不会影响对正在进行的章节的理解。
为了冻结任何行版本,VACUUM 必须检查几个条件
xmax
必须为零,因为只有未删除的行才能永远可见。xmin
必须比所有当前存在的事务oldest(pg_stat_activity.backend_xmin)
旧。这保证了没有现有事务可以修改或删除该版本。xmin
和xmax
的事务必须已提交。
冻结操作在什么时候发生?请注意,存在一些名为 xxx_age
的配置参数。它们定义了距离 - 主要到 pg_current_xact_id
-,VACUUM 的操作将从这些距离开始。在这种情况下,'age' 并不意味着特定时间段,它始终是一个纯数字,表示事务数,例如 5000 万。另外请注意,VACUUM 始终读取完整的物理页面,并在其中找到的行版本上进行操作。
- 当客户端发出带有 FREEZE 选项的 SQL 命令 VACUUM 时。在这种情况下,将处理受影响表的所有页面,这些页面在可见性地图 中被标记为可能包含未冻结行。
- 当客户端发出没有任何选项的 SQL 命令 VACUUM,并且存在比 vacuum_freeze_table_age(默认值:15000 万)减去 vacuum_freeze_min_age(默认值:5000 万)旧的 XID 时。与之前一样,将处理在可见性地图中被标记为可能包含未冻结行的所有页面。
- 当 Autovacuum 进程运行时。此类进程以两种模式之一运行:在正常模式下,它会跳过具有比 vacuum_freeze_min_age(默认值:5000 万)年轻的行版本的页面,只作用于所有 XID 都较旧的页面。跳过较年轻的 XID 可以防止对这些页面进行处理,因为这些页面很可能被将来的 SQL 命令之一更改。如果进程发现正在处理的表的最新 XID 超过 vacuum_freeze_table_age(默认值:15000 万),它将切换到激进模式。在激进模式下,Autovacuum 处理受影响表的全部页面。
VACUUM 和 Autovacuum 知道每个表的最旧未冻结 XID 向前移动到了哪个值,并将该值记录在pg_class.relfrozenxid 中。该值与pg_current_xact_id
分割点的距离会变小(可能存在未冻结的行),而到环绕点 pg_current_xact_id +
的距离会变大(只有冻结的行)。这就是冻结如何跟随移动的“过去”/“未来”边界。
注意:在 PostgreSQL 9.4 版本之前,冻结算法将值“2”(FrozenTransactionId)存储在xmin
中,而不是在t_infomask
中设置标志。