这篇博文讨论了 PostgreSQL 中多版本并发控制的基础知识。然后介绍快照以及它们如何控制元组的可见性。还讨论了与表扫描 API 的集成。
与许多关系数据库管理系统一样,PostgreSQL 使用多版本并发控制(MVCC)来支持并行运行的事务,并协调对图元的并行访问。
- 快照用于确定图元的哪个版本在哪个事务中可见。
- 每个修改数据的事务都有一个事务 ID(txid)。
- 图元与两个属性(xmin、xmax)一起存储,这两个属性决定了图元在哪个快照(以及哪个事务)中可见。
本博文将讨论快照的一些实现细节。
元组可见性
本文使用下表来说明快照在 PostgreSQL 中的工作原理。
CREATE TABLE temperature ( |
让我们在此表中插入第一条记录:这是通过创建一个新事务、获取当前事务 ID(如果可用)、插入新元组、再次获取事务 ID 并提交事务来完成的。
BEGIN; |
从这个示例中可以看出,PostgreSQL 只在数据被修改后立即为事务分配一个事务 ID,这一点很重要。这样做是为了防止不必要的工作,并防止事务 ID 耗尽。即使事务 ID 是一个 32 位整数,该值也会在某个时刻耗尽。PostgreSQL 可以处理这种溢出(即冻结元组,以正确处理事务 ID 包络)。
系统属性 xmin 和 xmax 决定了能看到某个元组的第一个事务和最后一个事务。此外,ctid 属性显示元组在相应页面上的编号。
当 SELECT 语句中明确提到这些属性时,就会显示它们的值:
SELECT xmin, xmax, ctid, * FROM temperature; |
输出结果表示:
- 所有事务 ID >= 5062286 的事务都能看到这个元组。
- 删除元组时,xmax 值将填入能看到此元组的最大事务 ID。
- ctid为 0,1 表示该图元是第 0 页的第一个图元。
现在我们删除该图元:
EGIN; |
但是,当执行 SELECT 语句时,什么也不会返回,而是返回一个带有 xmin 和 xmax 值的元组。
SELECT xmin, xmax, ctid, * FROM temperature; |
产生这种行为的原因是内部扫描器。如果一个元组在当前事务快照中不可见。要从元组中获取这些值,我们需要使用更底层的工具,而不是简单的 SELECT。
PostgreSQL 的 pageinspect 扩展允许我们获取存储在页面上的所有元组,并解码内部标志和属性。需要加载该扩展,然后就可以检查关系的页面了。
-- Load the extension |
输出结果显示,第 0 页的第一个元组(上述输出中的ctid 为 (0,1))的 t_max 值为 5062291,与删除该元组的事务 ID 相同。因此,每个事务 ID 大于 5062291 的事务都看不到这个元组。
快照
PostgreSQL 扫描表时,必须指定快照。请参见 table_beginscan 函数,该函数将快照数据作为第二个参数:
static inline TableScanDesc table_beginscan(Relation rel, |
内部数据结构
通常,事务快照 transaction snapshot被用作该函数的参数。结构 SnapshotData 包含快照的所有信息。在本博文中,我们将重点讨论以下属性:
typedef struct SnapshotData |
字段 xmin 定义了系统中最老的活动事务。所有 txid 小于此值的事务都已提交。xmax 包含快照已知的最新事务 ID。当前快照不可见 txid > xmax 的所有图元。
为什么需要 xip 和 xcnt 字段?
对于 xmin 和 xmax 之间的事务 ID,需要确定创建快照时事务是已提交还是正在进行中。
DBMS 处理多个用户的查询。他们可以随时启动事务。这些事务的开始时间和提交时间没有顺序。这意味着在创建快照时,可能有事务 ID 大于 xmin 的事务已经提交。然而,在 [xmin, xmax] 范围内的其他一些事务仍未提交。由于需要正确处理已提交和未提交事务的数据,因此定义了一个长度为 xcnt 的事务 ID 数组 xip。它包含所有大于 xmin 且小于 xmax 的事务,这些事务在快照拍摄时正在进行中。
示例
为了说明这种行为,让我们用三个事务做一个实际例子。
事务1
BEGIN; |
第一个事务在表temperature 中插入新数据,但保持未提交状态。该事务的事务 ID 是 5062310。
事务2
BEGIN; |
此外,第二个事务向同一个表插入数据,但也保持未提交状态。该事务的 ID 是 5062311。
事务3
SELECT * FROM pg_current_snapshot(); |
第三个事务使用函数 pg_current_snapshot 获取当前快照。函数的输出意味着,ID 小于 5062310 的事务的所有更改都是可见的。等于或大于事务 ID 5062310 的更改不可见,此时不存在未提交的事务。
那么,仍未提交的事务 5062310 和 5062311 又是怎么回事呢?由于在这个演示系统中到目前为止还没有提交更多事务,所以 PostgreSQL 没有更改当前事务 ID。不过,这是可以改的:
SELECT * FROM pg_current_xact_id_if_assigned(); |
与函数 pg_current_xact_id_if_assigned 不同,函数 pg_current_xact_id 强制为当前事务指定一个事务 ID。在我们的例子中,它是 5062312。使用该事务 ID 也会导致快照的更新。
第一个值保持不变。不过,ID 小于 5062310 的事务修改的所有图元在当前快照中都是可见的。但是,上限 (xmax) 发生了变化。现在,所有等于或大于 5062313 的更改在当前快照中都不可见。因为我们的事务 ID 是 5062312,所以这些更改不可见是合理的。那么新的 5062310,5062311 部分呢?这是快照的 xip 部分,表示在拍摄快照时,5062310 和 5062311 这两个事务尚未提交。因此,这些更改在当前快照中也不可见。一旦这些事务中的一个提交,并且我们拍摄了新快照,事务 ID 就会从 zip 中移除,因此这些更改在当前快照中就会可见。
导出快照
PostgreSQL 另一个有趣的功能是导出快照并将其加载到其他会话中。调用 pg_export_snapshot 函数可以导出快照。该函数会返回快照的 ID,并在数据目录的 pg_snapshots 文件夹中创建一个相应的文件。
BEGIN; |
该文件包含的信息与上面讨论过的 pg_current_snapshot 所返回的信息相同。此外,它还包含有关所用隔离级别或所用数据库 ID 的更多信息。
$ cat ~/postgresql-sandbox/data/REL_15_1_DEBUG/pg_snapshots/0000000C-000005F6-1 |
可以通过调用 SET TRANSACTION SNAPSHOT 0000000C-000005F6-1 将导出的快照加载到另一个事务中,从而使用与创建快照的事务相同的快照运行。
快照和事务隔离级别
- 根据隔离级别的不同,快照会在事务启动时(可重复读取)或为事务中的每条语句(已提交读取)创建。
- 当为事务中的每条语句创建新快照时,其他事务中已提交的数据就会在当前事务中可见。
- 如果只为整个事务创建一个快照,xmax 值就会保持不变,ID 更高的事务中的新数据就不会可见,读取也是可重复的。