1
什么是可见性指标?
简单来说,可见性指的是在特定的状态、后台进程或事务中,一行数据(默认为堆元组)是否应该向用户显示。
例如:
-
用户通过UPDATE将数据记录从‘A’更改为‘B’。
-
PostgreSQL 通过INSERT新记录‘B’的方式处理,同时将记录‘A’标记为‘不可见’。
-
系统中同时存在这两条记录,但‘B’是可见的,‘A’则不可见。
-
已被删除或不可见的记录也被称为‘死’元组。
-
VACUUM进程的职责之一是清除这些‘死’元组以释放空间。
MVCC(多版本并发控制)作为一种方法,在每次写操作都会创建数据的“新版本”,同时保留“旧版本”。该方法允许并发的读写操作而不会出现相互阻塞。PostgreSQL使用MVCC的变体(也称快照隔离)来隔离并发事务。
因此,单个数据可能会有多个“版本”,PostgreSQL负责根据多种因素确定应向用户呈现哪个“版本”。这个过程也被称为“可见性检查”或“可见性控制”。
在本文中,我们将深入探讨PostgreSQL的可见性检查机制,以了解其工作原理。
2
可见性指标
据我所知,PostgreSQL 根据以下指标确定数据的可见性:
-
Transaction ID
-
xmin
-
xmax
-
cid
-
transaction snapshot
-
CLOG (Commit Log)
-
hintbit
这些指标到底是什么?让我们来一探究竟。
3
事务 ID
这应该是不言自明的。在 PostgreSQL 中,所有事务都与一个被称为事务ID的编号相关联。这主要用于检查相关数据是否在当前事务中插入或删除。我们将在文章的后面部分对此进行进一步探讨。
xmin, xmax, cid 和hintbit
这些指标被归为一类,因为它们都存储在每个堆数据元组的头部,并且每个数据元组可能具有不同的值,从而导致不同的可见性检查结果。下面的图像显示了它们存储在元组头部的位置:
xmin:
这是插入该堆数据元组的事务ID
xmax:
-
这是删除该堆数据元组的事务ID。
-
如果元组头部含有有效的、已提交的 xmax 值,通常说这个元组是“已删除的”或“不可见的”。
cid:
-
这是在事务中插入此数据元组的命令ID。
-
主要用于确定当前事务中的可见性。
-
如果一个事务包含 SELECT 和其他 DML 查询(INSERT、UPDATE 和 DELETE),那么可以使用cid 来确定一个数据元组是在 SELECT 之前还是之后被插入或删除。
hintbit:
-
这是一个标志字段,用于存储来自CLOG的查询结果,这样PostgreSQL就不必每次都查找CLOG。CLOG指的是提交日志,下文会详细解释。
-
如果hintbit设置了HEAP_XMIN_COMMITTED标志,这意味着插入此元组的事务ID(即xmin)已提交。
-
如果PostgreSQL可以直接从hintbit获取这些信息,那么它可以节省一些查找CLOG的时间,从而提高性能。
-
可能的hintbit标志值如下:
4
CLOG (Commit Log)
CLOG 是存储在共享内存中并持久化在 $PGDATA/pg_xact 目录下的数据结构。CLOG 的目的是记录事务的提交状态。状态可以是以下之一:
-
COMMITTED
-
IN_PROGRESS
-
ABORTED
-
SUB_COMMITTED
例如:
如果一个元组的 xmin 是 103,而 CLOG 显示 103 是 ABORTED(如下图所示),这意味着 103 不可见,因为插入该数据元组的事务已经 ABORTED(即中止)。为了提高性能,PostgreSQL 将会标记 hintbit 为 HEAP_XMIN_INVALID,这样下次再访问同样的数据元组时就不需要再从 CLOG 中查找了。
5
事物快照
事务快照是另一个数据结构,用于存储某个时间点的活跃事务信息。它主要用来在并发事务中隔离数据元组。一个数据元组可能被另一个事务插入并提交了,但是我持有的事务快照可能是过去的。在这种情况下,根据我过去的快照,这个数据元组对我来说仍然是不可见的。
一个事务快照可以由几个事务号来表示:
[xmin, xmax, xip_list]
-
xmin: 仍然活跃的最小事务ID — 小于这个值的事务已经提交或回滚。
-
xmax: 已提交或回滚的最大事务ID + 1 — 大于或等于这个值的事务尚未提交或回滚。
-
xip_list: 正在进行中的事务号列表 — 应该大于xmin并且小于xmax。
请注意,事务快照中的 xmin 和 xmax 的定义与上文中存储在元组头部的 xmin 和 xmax 是不同的。
通过示例可以更好的解释事务快照。
假设我们有三个并发的后端进程 A、B 和 C,分别启动事务号为 747、748 和 749,每个事务插入了数据记录 'A'、'B' 和 'C'。
postgres=# BEGIN;
Postgres*# INSERT INTO mytable VALUES('A');
Postgres*# SELECT txid_current();
txid_current
--------------
747
(1 row)
postgres=# BEGIN;
postgres=# INSERT INTO mytable VALUES('B');
postgres=# SELECT txid_current();
txid_current
--------------
748
(1 row)
postgres=# BEGIN;
Postgres*# INSERT INTO mytable VALUES('C');
Postgres*# SELECT txid_current();
txid_current
--------------
749
(1 row)
然后,我们提交了事务 749,并通过 "pg_current_snapshot()" 获取了当前的事务快照。
postgres=# BEGIN;
Postgres*# INSERT INTO mytable VALUES('A');
Postgres*# SELECT txid_current();
txid_current
--------------
747
(1 row)
postgres=# BEGIN;
postgres=# INSERT INTO mytable VALUES('B');
postgres=# SELECT txid_current();
txid_current
--------------
748
(1 row)
postgres=# BEGIN;
Postgres*# INSERT INTO mytable VALUES('C');
Postgres*# SELECT txid_current();
txid_current
--------------
749
(1 row)
Postgres*# COMMIT;
postgres=# select pg_current_snapshot();
pg_current_snapshot
---------------------
747:750:747,748
(1 row)
从后端进程 C 的角度来看,事务快照显示:
-
747 是仍然活跃的最小事务ID。
-
750 是已提交或已中止的最大事务ID + 1(749 + 1)。
-
747 和 748 是仍在进行中的事务。
这意味着:
-
C 能够看到由事务ID小于750的数据元组插入的数据,但不能看到事务ID为747和748的数据,因为它们仍在进行中(尚未提交或回滚)。
-
C 不能看到未来插入的数据元组(事务ID大于750的数据)。
-
C 能够看到由自己在事务749中插入的数据元组,因为它刚刚提交了这个事务。
我们也可以在事务进行中查看 A 和 B 的事务快照。
postgres=# BEGIN;
Postgres*# INSERT INTO mytable VALUES('A');
Postgres*# SELECT txid_current();
txid_current
--------------
747
(1 row)
Postgres*# select pg_current_snapshot();
pg_current_snapshot
---------------------
747:750:748
(1 row)
postgres=# BEGIN;
postgres=# INSERT INTO mytable VALUES('B');
postgres=# SELECT txid_current();
txid_current
--------------
748
(1 row)
postgres=*# select pg_current_snapshot();
pg_current_snapshot
---------------------
747:750:747
(1 row)
postgres=# BEGIN;
Postgres*# INSERT INTO mytable VALUES('C');
Postgres*# SELECT txid_current();
txid_current
--------------
749
(1 row)
Postgres*# COMMIT;
postgres=# select pg_current_snapshot();
pg_current_snapshot
---------------------
747:750:747,748
(1 row)
如您所见,A 的事务快照表明 B(748)正在进行中,因此它还不能看到 B(748)插入的数据元组。事务 747 是当前 A 所在的事务,但在快照中没有标记,因此它可以看到当前事务中由自己插入的数据。B 的事务快照表明情况恰恰相反。
6
确定可见性
上面提到的可见性指标只是部分指标;这意味着我们不能仅凭其中一个或少数几个指示来确定数据元组的真实可见性。必须考虑全部指标来计算正确的可见性。
让我们总结一下决定可见性的因素:
-
xmin、xmax、cid 和 hintbit 存储在元组头部。
-
快照(snapshot)、事务ID在启动事务时由事务管理器提供。
-
CLOG 存储在共享内存中
下图是可见性检查的简化说明:
这只是对整个可见性检查过程的简要概述。该过程可以在 src/backend/heap/heapam_visibility.c 文件的 heapTupleSatisfiesMVCC() 函数中找到。当然,实际的逻辑比上面的示意图更复杂。
我已经研究了 PostgreSQL 可见性检查逻辑的整个过程,并制作了下面更详细的流程图:可见性检查可以像这张图展示的那样深入,但由于使用了hintbit,只需通过使用数据元组的 xmin 和 xmax 值检查 hintbit 和快照,就可以很快完成大多数可见性检查。
7
总结
如果您是一名在从事分布式数据库、共享存储集群或分片技术的 PostgreSQL 开发者,我相信本文能为您提供有关 PostgreSQL 如何处理可见性检查的见解。当我们开始与多个数据库节点交互时,数据的原子性和可见性始终是一个问题。如何确保整个集群中的数据一致性是一个常见的挑战。了解事务和可见性的基本原理可以帮助我们解决这些问题。