陈老师:1415968548 郑老师:2735197625 乐老师:354331153
客服热线:
19941464235/19906632509(微信同号)

客服微信

【PostgreSQL】PG的多版本并发控制

作者:炎燚小寶
原创
发布时间:2023-12-21 11:46
浏览量:271

注: 本文为云贝教育 刘峰 原创,请尊重知识产权,转发请注明出处,不接受任何抄袭、演绎和未经注明出处的转载。


并发是一种当多个事务在数据库中并发运行时维护原子性和隔离性的机制,这是 ACID 的两个属性。

并发控制技术主要分为三种:多版本并发控制(MVCC)、严格两相锁(S2PL)和乐观并发控制(OCC)。每种技术都有许多变化。在 MVCC 中,每次写入操作都会创建数据项的新版本,同时保留旧版本。当事务读取数据项时,系统会选择其中一个版本以确保单个事务的隔离性。MVCC 的主要优点是“读取器不会阻止写入器,写入器也不会阻止读取器”,相比之下,例如,基于 S2PL 的系统在写入器写入项目时必须阻止读取器,因为写入器获得独占性锁定该项目。 PostgreSQL 和一些 RDBMS 使用 MVCC 的变体,称为快照隔离 (SI)。

为了实现 SI,一些 RDBMS(例如 Oracle)使用回滚段。当写入新数据项时,旧版本的数据项被写入回滚段,随后新数据项被覆盖到数据区。 PostgreSQL 使用更简单的方法。新的数据项将直接插入到相关的表页中。读取项目时,PostgreSQL 通过应用可见性检查规则来选择项目的适当版本来响应单个事务。

SI 不允许 ANSI SQL-92 标准中定义的三种异常:脏读、不可重复读和幻读。然而,SI 无法实现真正的可序列化,因为它允许序列化异常,例如 Write Skew 和 Read-only Transaction Skew。请注意,基于经典可串行性定义的 ANSI SQL-92 标准并不等同于现代理论中的定义。为了解决此问题,从版本 9.1 开始添加了可串行快照隔离 (SSI)。 SSI可以检测序列化异常并可以解决此类异常引起的冲突。因此,PostgreSQL 9.1 或更高版本提供了真正的 SERIALIZABLE隔离级别。 (此外,SQL Server 也使用 SSI;Oracle 仍然只使用 SI。)

PostgreSQL 中的事务隔离级别

下表描述了 PostgreSQL 实现的事务隔离级别:

*1:在版本 9.0 及更早版本中,此级别已用作“SERIALIZABLE”,因为它不允许 ANSI SQL-92 标准中定义的三种异常。然而,随着版本 9.1 中 SSI 的实现,该级别已更改为“REPEATABLE READ”,并引入了真正的 SERIALIZABLE 级别。


一. 事务ID

每当事务开始时,事务管理器都会分配一个唯一标识符,称为事务 ID (txid)。 PostgreSQL 的txid 是一个 32 位无符号整数,大约 42 亿(千万)。如果在交易开始后执行内置的txid_current() 函数,该函数将返回当前的 txid,如下所示:


testdb=# BEGIN;
BEGIN
testdb=# SELECT txid_current();
 txid_current
--------------
          825
(1 row)


PostgreSQL 保留以下三个特殊的 txid:

0 表示 txid 无效。

1表示Bootstrap txid,仅在数据库集群初始化时使用。

2表示Frozen txid。

Txids 可以相互比较。例如,从 txid 100 的角度来看,大于 100 的 txid 是“未来的”,并且从txid 100 中是不可见的;小于 100 的 txids 是“过去的”并且可见(图 1.1 a))。

图 1.1。 PostgreSQL 中的事务 ID。

由于实际系统中txid空间不足,PostgreSQL将txid空间视为一个圆。前面的 21 亿个 txid 是“过去的”,接下来的 21 亿个 txid 是“未来的”(图 1.1 b)。

请注意,BEGIN 命令不会分配 txid。在PostgreSQL中,执行BEGIN命令后执行第一个命令时,事务管理器会分配一个tixd,然后事务开始。

二. 行结构

表页中的堆元组分为两种类型:普通数据元组和TOAST元组。本节仅描述常用的元组。

堆元组由三部分组成:HeapTupleHeaderData 结构、NULL 位图和用户数据(图 1.2)。

HeapTupleHeaderData 结构包含七个字段,但后续部分只需要其中四个字段:

• t_xmin 保存插入该元组的事务的 txid。

• t_xmax 保存删除或更新此元组的事务的 txid。如果这个元组没有被删除或更新,t_xmax被设置为0,这意味着INVALID。

• t_cid 保存命令id(cid),它是当前事务内执行此命令之前执行的SQL命令的数量,从0开始。例如,假设我们在单个事务内执行三个INSERT命令:'BEGIN;INSERT;INSERT;INSERT;commit;'。如果第一个命令插入此元组,则 t_cid 设置为 0。如果第二个命令插入此元组,则 t_cid 设置为 1,依此类推。

• t_ctid 保存指向自身或新元组的元组标识符(tid)。 tid,如第 3. 节所述,用于标识表中的元组。当这个tuple更新的时候,这个tuple的t_ctid就指向新的tuple;否则,t_ctid 指向其自身。


PG插件pageinspect

1、安装
testdb=# create extension pageinspect;
CREATE EXTENSION


2、创建表
testdb=# create table t1 as select * from (select row_number() over() as row num,oid from pg_class) t where rownum<2; SELECT 1 3、查看数据 testdb=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid FROM heap_page_items(get_raw_page('t1', 0)); tuple | t_xmin | t_xmax | t_cid | t_ctid -------+--------+--------+-------+-------- 1 | 850 | 871 | 0 | (0,1) 2 | 851 | 854 | 1 | (0,3) 3 | 854 | 863 | 0 | (0,4) 4 | 863 | 865 | 0 | (0,5) 5 | 865 | 869 | 0 | (0,7) 6 | 866 | 0 | 0 | (0,6) 7 | 869 | 0 | 0 | (0,7) (7 rows)


不用插件,查看pg_attribute

1、查询pg_attribute表,查看添加到表中的系统列以及两个列id和name:


postgres=# SELECT attname, format_type (atttypid,atttypmod) FROM pg_attribute
WHERE attrelid = 'foo.bar'::regclass::oid ORDER BY attnum;
attname | format_type
----------+----------------------
tableoid | oid
cmax | cid
xmax | xidCluster Management Techniques
cmin | cid
xmin | xid
ctid | tid
  id | integer
name | character varying(5)
 (8 rows)


ctid来查询每个元组的位置·

通过显式查询xmin,我们可以通过查找每个记录的xmin值来查看插入记录的事务ID。注意以下日志中所有记录的xmin值:

我们还可以通过显式地选择每条记录来找到它的xmax。如果xmax为0,说明它从未被删除,并且是可见的

• cmin和cmax介绍

• cmin:插入事务中的命令标识符(从0开始)。

• cmax:删除事务中的命令标识符(从0开始)。

• cmin和cmax都是表示tuple的command id,即cmin是产生该条tuple的command id,cmax是删除该tuple的command id。

• cmin表示插入数据的command id,cmax表示删除数据的。

那怎么通过一个字段就能识别是插入还是删除呢?想要解决这个问题,我们需要了解下combo cid。

当我们的事务中只是插入数据时,t_cid存储的就是cmin,因为此时也只有cmin是有效的。而当进行了update或者delete操作时,才会产生cmax。当这种既有cmin又有 cmax的情况,即在同一个事务中既有插入又有更新的时候,t_cid存储的就是combo cid,当事务中既有插入又有更新的时候,t_cid存储的便是combo cid。

三.增删改行

本节介绍如何插入、删除和更新元组。然后,简要描述用于插入和更新元组的自由空间映射(FSM)。

为了重点关注元组,页眉和行指针在下面不再表示。图 3. 显示了如何表示元组的示例。

图 1.3。元组的表示。

3.1. 插入

通过插入操作,一个新的元组被直接插入到目标表的一页中(图1.4)。

图 1.4。元组插入。

假设txid为99的事务在页面中插入了一个元组。此时,插入的元组的头字段设置如下。

元组_1:

t_xmin 设置为 99,因为该元组是由 txid 99 插入的。

t_xmax 设置为 0,因为该元组尚未被删除或更新。

t_cid 设置为 0,因为该元组是 txid 99 插入的第一个元组。

t_ctid 设置为 (0,1),它指向自身,因为这是最新的元组。

3.2. 删除

在删除操作中,目标元组被逻辑删除。执行DELETE命令的txid的值被设置为元组的t_xmax(图1.5)。

图 1.5。元组删除。

假设元组Tuple_1被txid 111删除。此时,Tuple_1的头字段设置如下:

元组_1:

t_xmax 设置为 111。

如果提交了 txid 111,则不再需要 Tuple_1。通常,不需要的元组在 PostgreSQL 中被称为死元组。

死元组最终应该从页面中删除。清理死元组称为 VACUUM 处理,这将在第 6 章中进行描述。

3.3.修改

在更新操作中,PostgreSQL逻辑上删除最新的元组并插入一个新的元组(图1.6)。

图 1.6。将该行更新两次。

假设由 txid 99 插入的行被 txid 100 更新了两次。

当执行第一个 UPDATE 命令时,通过将 t_xmax 设置 txid 100 来逻辑删除 Tuple_1,然后插入Tuple_2。然后,Tuple_1的t_ctid被重写为指向Tuple_2。 Tuple_1和Tuple_2的头字段如下:

元组_1:

t_xmax 设置为 100。

t_ctid 从 (0, 1) 重写为 (0, 2)。

元组_2:

t_xmin 设置为 100。

t_xmax 设置为 0。

t_cid 设置为 0。

t_ctid 设置为 (0,2)。

当执行第二个UPDATE命令时,与第一个UPDATE命令一样,Tuple_2被逻辑删除并插入Tuple_3。 Tuple_2和Tuple_3的头字段如下:

元组_2:

t_xmax 设置为 100。

t_ctid 从 (0, 2) 重写为 (0, 3)。

元组_3:

t_xmin 设置为 100。

t_xmax 设置为 0。

t_cid 设置为 1。

t_ctid 设置为 (0,3)。

与删除操作一样,如果 txid 100 被提交,Tuple_1 和 Tuple_2 将成为死元组,并且,如果 txid 100 被中止,Tuple_2 和 Tuple_3 将成为死元组。

3.4. 空闲空间地图

当插入堆或索引元组时,PostgreSQL使用相应表或索引的FSM来选择可以插入的页。

正如 1.2.3 节中提到的,所有表和索引都有各自的 FSM。每个 FSM 存储有关相应表或索引文件中每个页的可用空间容量的信息

所有 FSM 都以“fsm”后缀存储,如有必要,它们会加载到共享内存中。

pg_freespacemap

扩展 pg_freespacemap 提供指定表/索引的可用空间。以下查询显示指定表中每个页面的可用空间比率。


testdb=# CREATE EXTENSION pg_freespacemap;
CREATE EXTENSION

testdb=# SELECT *, round(100 * avail/8192 ,2) as "freespace ratio"
FROM pg_freespace('accounts');
 blkno | avail | freespace ratio
-------+-------+-----------------
     0 |  7904 |           96.00
     1 |  7520 |           91.00
     2 |  7136 |           87.00
     3 |  7136 |           87.00
     4 |  7136 |           87.00
     5 |  7136 |           87.00


四. 提交日志 (clog)

PostgreSQL 在提交日志中保存事务的状态。提交日志(通常称为 clog)被分配到共享内存并在整个事务处理过程中使用。

本节介绍 PostgreSQL 中事务的状态、clog 的运行方式以及 clog 的维护。

4.1 事务状态

PostgreSQL 定义了四种事务状态:IN_PROGRESS、COMMITTED、ABORTED 和SUB_COMMITTED。

前三种状态是不言自明的。例如,当事务正在进行时,其状态为 IN_PROGRESS。

SUB_COMMITTED是针对子事务的,本文档中省略其描述。

4.2. CLOG运作过程

该clog由共享内存中的一个或多个 8 KB 页组成。它在逻辑上形成一个数组,其中数组的索引对应于各自的事务id,数组中的每一项保存对应事务id的状态。图 1.7 显示了clog及其工作原理。

图 1.7。clog如何运作。

T1:txid 200 次提交; txid 200 的状态从 IN_PROGRESS 更改为 COMMITTED。

T2:txid 201 中止; txid 201 的状态从 IN_PROGRESS 更改为 ABORTED。


当当前的 txid 前进并且 clog 无法再存储它时,会附加一个新页面。

当需要事务状态时,将调用内部函数。这些函数读取clog并返回所请求事务的状态。

4.3. 维护CLOG

当 PostgreSQLE服务关闭或检查点进程运行时,clog 的数据将写入存储在 pg_xact 子目录中的文件中。 (请注意,pg_xact 在 9.6 或更早版本中称为 pg_clog。)这些文件被命名为0000、0001 等。最大文件大小为 256 KB。例如,如果clog使用8页(第一页到第八页,总大小为64 KB),则其数据写入0000(64 KB)。如果clog使用37页(296 KB),则其数据被写入0000和0001,其大小分别为256 KB和40 KB。

当 PostgreSQL 启动时,会加载 pg_xact 文件中存储的数据来初始化 clog。

clog的大小不断增加,因为每当clog被填满时就会附加新的页面。然而,并非clog中的所有数据都是必需的。vacuum处理会定期删除此类旧数据(clog页面和文件)。

五. 事务快照

事务快照是一个数据集,它存储有关单个事务的某个时间点的所有事务是否都处于活动状态的信息。这里的活动事务意味着它正在进行或尚未开始。

PostgreSQL内部定义事务快照的文本表示格式为“100:100:”。例如,“100:100:”表示“小于 99 的 txids 不活跃,等于或大于 100 的 txids 活跃”。

内置函数 pg_current_snapshot 及其文本表示格式

函数 pg_current_snapshot 显示当前事务的快照。


testdb=# SELECT pg_current_snapshot();
 pg_current_snapshot
---------------------
 100:104:100,102
(1 row)


txid_current_snapshot 的文本表示为“xmin:xmax:xip_list”,其组成部分描述如下:

• xmin

(仍处于活动状态的最早的 txid):所有较早的事务将要么提交并可见,要么回滚并终止。

• xmax

(第一个尚未分配的 txid):所有大于或等于该值的 txid 在快照时尚未启动,因此不可见。

• xip_list

(快照时的活动交易 ID 列表):该列表仅包含 xmin 和 xmax 之间的活动交易 ID。

例如,在快照“100:104:100,102”中,xmin 为“100”,xmax 为“104”,xip_list 为“100,102”。

下面是两个具体的例子:

图 1.8。事务快照表示的示例。


第一个示例是“100:100:”。该快照的含义如下(图 1.8(a)):

等于或小于 99 的 txids 不处于活动状态,因为 xmin 为 100。

等于或大于 100 的 txids 处于活动状态,因为 xmax 为 100。

第二个示例是“100:104:100,102”。该快照的含义如下(图 1.8(b)):

等于或小于 99 的 txids 不活跃。

等于或大于 104 的 txids 处于活动状态。

txid 100 和 102 处于活动状态,因为它们存在于 xip 列表中,而 txid 101 和 103 则处于不活动状态。

事务快照由事务管理器提供。在READ COMMITTED隔离级别下,每当执行SQL命令时,事务都会获取快照;否则(REPEATABLE READ 或 SERIALIZABLE),事务仅在执行第一个 SQL 命令时获取快照。获得的事务快照用于元组的可见性检查,这在1.7节中描述。

当使用获取的快照进行可见性检查时,快照中的活动事务必须被视为正在进行中,即使它们实际上已提交或中止。此规则很重要,因为它会导致 READ COMMITTED 和 REPEATABLE READ(或 SERIALIZABLE)之间的行为差异。我们在以下各节中反复引用此规则。

在本节的其余部分中,事务管理器和事务将使用特定场景图 1.9 进行描述。


事务管理器始终保存有关当前正在运行的事务的信息。假设三个事务相继启动,Transaction_A和Transaction_B的隔离级别为READ COMMITTED,Transaction_C的隔离级别为REPEATABLE READ。

T1:

Transaction_A 启动并执行第一个 SELECT 命令。当执行第一个命令时,Transaction_A 请求当前时刻的 txid 和快照。在这种情况下,事务管理器分配 txid 200,并返回事务快照“200:200:”。

T2:

Transaction_B 启动并执行第一个 SELECT 命令。事务管理器分配 txid 201,并返回事务快照“200:200:”,因为 Transaction_A (txid 200) 正在进行中。因此,从Transaction_B中看不到Transaction_A。

T3:

Transaction_C 启动并执行第一个 SELECT 命令。事务管理器分配txid 202,并返回事务快照'200:200:',因此从Transaction_C中看不到Transaction_A和Transaction_B。

T4:

Transaction_A 已提交。事务管理器删除有关该事务的信息。

T5:

Transaction_B 和 Transaction_C 执行各自的 SELECT 命令。


Transaction_B 需要事务快照,因为它处于 READ COMMITTED 级别。在这种情况下,Transaction_B 获得新快照“201:201:”,因为 Transaction_A (txid 200) 已提交。因此,Transaction_A 不再对 Transaction_B 不可见。Transaction_C 不需要事务快照,因为它处于 REPEATABLE READ 级别并使用获取的快照,即“200:200:”。因此,Transaction_A 对于 Transaction_C 仍然是不可见的。

六. 可见性检查规则

可见性检查规则是一组规则,用于使用元组的 t_xmin 和 t_xmax、clog 以及获取的事务快照来确定每个元组是否可见或不可见。这些规则太复杂,无法详细解释。因此,本文档显示了后续描述所需的最低规则。下面,我们省略与子事务相关的规则,并忽略关于 t_ctid 的讨论,即我们不考虑在事务内更新两次以上的元组。

所选规则的数量为十个,可以分为三种情况。

6.1. t_xmind状态为ABORT

t_xmin 状态为 ABORTED 的元组始终不可见(规则 1),因为插入该元组的事务已被中止。

/* t_xmin status == ABORTED */
Rule 1: IF t_xmin status is 'ABORTED' THEN
                  RETURN 'Invisible'
            END IF


该规则明确地表达为以下数学表达式。

• Rule 1: If Status(t_xmin) = ABORTED ⇒ Invisible

6.2. t_xmind状态为IN_PROGRESS

t_xmin 状态为 IN_PROGRESS 的元组本质上是不可见的(规则 3 和 4),除非在一种情况下。

/* t_xmin status == IN_PROGRESS */
IF t_xmin status is 'IN_PROGRESS' THEN
IF t_xmin = current_txid THEN
Rule 2: IF t_xmax = INVALID THEN
RETURN 'Visible'
Rule 3: ELSE /* this tuple has been deleted or updated by the cur
rent transaction itself. */
RETURN 'Invisible'
END IF
Rule 4: ELSE /* t_xmin ≠ current_txid */
RETURN 'Invisible'
END IF
END IF


如果这个元组是被另一个事务插入的,并且t_xmin的状态是IN_PROGRESS,那么这个元组显然是不可见的(规则4)。

如果 t_xmin 等于当前 txid(即该元组是由当前事务插入的)并且 t_xmax 不为 INVALID,则该元组是不可见的,因为它已被当前事务更新或删除(规则 3)。

异常情况是当前事务插入该元组且 t_xmax 为 INVALID 的情况。在这种情况下,这个元组必须对当前事务可见(规则2),因为这个元组是当前事务本身插入的元组。

Rule 2: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD ⇒ Visible

• Rule 3: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax ≠ INVAILD ⇒ Invisible


• Rule 4: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin ≠ current_txid ⇒ Invisible

6.3. t_xmind状态为COMMITTED

t_xmin 状态为 COMMITTED 的元组是可见的(规则 6,8 和 9),但以下三种情况除外。

/* t_xmin status == COMMITTED */
IF t_xmin status is 'COMMITTED' THEN
Rule 5: IF t_xmin is active in the obtained transaction snapshot THEN
RETURN 'Invisible'
Rule 6: ELSE IF t_xmax = INVALID OR status of t_xmax is 'ABORTED' THEN
RETURN 'Visible'
ELSE IF t_xmax status is 'IN_PROGRESS' THEN
Rule 7: IF t_xmax = current_txid THEN
RETURN 'Invisible'
Rule 8: ELSE /* t_xmax ≠ current_txid */
RETURN 'Visible'
END IF
ELSE IF t_xmax status is 'COMMITTED' THEN
Rule 9: IF t_xmax is active in the obtained transaction snapshot THEN
RETURN 'Visible'
Rule 10: ELSE
RETURN 'Invisible'
END IF
END IF
END IF


规则 6 很明显,因为 t_xmax 无效或已中止。三种例外情况以及规则 8 和 9 描述如下:

第一个例外情况是t_xmin在获取的事务快照中处于活动状态(规则5)。在这种情况下,该元组是不可见的,因为 t_xmin 应被视为正在进行中。

第二个例外情况是 t_xmax 是当前 txid(规则 7)。在这种情况下,与规则 3 一样,该元组是不可见的,因为它已被该事务本身更新或删除。

相反,如果 t_xmax 的状态为 IN_PROGRESS 并且 t_xmax 不是当前 txid(规则 8),则该元组可见,因为它尚未被删除。

第三个例外情况是t_xmax的状态为COMMITTED并且t_xmax在获取的事务快照中不活跃(规则10)。在这种情况下,该元组是不可见的,因为它已被另一个事务更新或删除。

相反,如果 t_xmax 的状态为 COMMITTED,但 t_xmax 在获取的事务快照中处于活动状态(规则 9),则该元组可见,因为 t_xmax 应被视为正在进行中。

• Rule 5: If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active ⇒ Invisible

Rule 6: If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) ⇒ Visible

Rule 7: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid ⇒ Invisible

Rule 8: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax ≠ current_txid ⇒ Visible

Rule 9: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active ⇒ Visible

• Rule 10: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) ≠ active ⇒ Invisible

七. 可见性检查

本节描述 PostgreSQL 如何执行可见性检查,这是在给定事务中选择适当版本的堆元组的过程。本节还介绍 PostgreSQL 如何防止 ANSI SQL-92 标准中定义的异常:脏读、可重复读和幻读。

7.1. 可见性检查

图 1.10 显示了描述可见性检查的场景。

图 1.10。描述可见性检查的场景。


在图1.10所示的场景中,SQL命令按照以下时序执行。

T1:开始交易(txid 200)

T2:开始交易(txid 201)

T3:执行txid 200和201的SELECT命令

T4:执行txid 200的UPDATE命令T5:执行txid 200和201的SELECT命令

T6:提交 txid 200

T7:执行txid 201的SELECT命令

为了简化描述,假设只有两个事务,即txid 200和201。txid 200的隔离级别是READ COMMITTED,txid 201的隔离级别是READ COMMITTED 或 REPEATABLE READ。

我们探讨 SELECT 命令如何对每个元组执行可见性检查。


SELECT commands of T3:

在 T3 时,表 tbl 中只有 Tuple_1,并且根据规则 6 可见。因此,两个事务中的 SELECT 命令都返回“Jekyll”。

• Rule6(Tuple_1) ⇒ Status(t_xmin:199) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible

testdb=# -- txid 200
testdb=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)

testdb=# -- txid 201
testdb=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)


SELECT commands of T5:

首先,我们探索 txid 200 执行的 SELECT 命令。根据规则 7,Tuple_1 不可见,而根据规则2,Tuple_2 可见。因此,该 SELECT 命令返回“Hyde”。

• Rule7(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) =IN_PROGRESS ∧ t_xmax:200 = current_txid:200 ⇒ Invisible


• Rule2(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧ t_xmin:200 =current_txid:200 ∧ t_xmax = INVAILD ⇒ Visible
testdb=# -- txid 200
testdb=# SELECT * FROM tbl;
name
------
Hyde
(1 row)

另一方面,在 txid 201 执行的 SELECT 命令中,Tuple_1 根据规则 8 可见,而 Tuple_2 根据规则 4 不可见。因此,该 SELECT 命令返回“Jekyll”。

• Rule8(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = IN_PROGRESS ∧ t_xmax:200 ≠ current_txid:201 ⇒ Visible

• Rule4(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧ t_xmin:200 ≠ current_txid:201 ⇒ Invisible

testdb=# -- txid 201
testdb=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)


如果更新的元组在提交之前从其他事务中可见,则这称为脏读,也称为 wr 冲突。然而,如上所示,在 PostgreSQL 的任何隔离级别下都不会发生脏读。


SELECT command of T7:

下面描述 T7 的 SELECT 命令在两个隔离级别下的行为。

当 txid 201 处于 READ COMMITTED 级别时,txid 200 被视为已提交,因为事务快照是“201:201:”。因此,根据规则 10,Tuple_1 不可见,而根据规则 6,Tuple_2 可见。SELECT命令返回“Hyde”。

• Rule10(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = COMMITTED ∧ Snapshot(t_xmax:200) ≠ active ⇒ Invisible

• Rule6(Tuple_2): Status(t_xmin:200) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible

testdb=# -- txid 201 (READ COMMITTED)
testdb=# SELECT * FROM tbl;
name
------
Hyde


请注意,在提交 txid 200 之前和之后执行的 SELECT 命令的结果是不同的。这通常称为不可重复读取。

相反,当 txid 201 处于 REPEATABLE READ 级别时,txid 200 必须被视为 IN_PROGRESS,因为事务快照是“200:200:”。因此,根据规则 9,Tuple_1 可见,根据规则 5,Tuple_2 不可见。SELECT 命令返回“Jekyll”。请注意,不可重复读取不会发生在可重复读取(和可串行化)级别中。

• Rule9(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = COMMITTED ∧ Snapshot(t_xmax:200) = active ⇒ Visible

• Rule5(Tuple_2): Status(t_xmin:200) = COMMITTED ∧ Snapshot(t_xmin:200) = active ⇒ Invisible、

testdb=# -- txid 201 (REPEATABLE READ)
testdb=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)


7.2. PostgreSQL 的 REPEATABLE READ 级别中的幻

读ANSI SQL-92 标准中定义的可重复读取允许幻读。然而,PostgreSQL 的实现不允许它们。原则上,SI 不允许幻读。

假设两个事务(即 Tx_A 和 Tx_B)同时运行。它们的隔离级别是 READ COMMITTED 和REPEATABLE READ,它们的 txid 分别是 100 和 101。首先,Tx_A 插入一个元组。然后,就承诺了。插入的元组的t_xmin为100。接下来,Tx_B执行SELECT命令;然而,根据规则 5,Tx_A 插入的元组是不可见的。因此,不会发生幻读。

• Rule5(new tuple): Status(t_xmin:100) = COMMITTED ∧ Snapshot(t_xmin:100) = active ⇒ Invisible

事务A
testdb=# -- Tx_A: txid 100
testdb=# START TRANSACTION
testdb-# ISOLATION LEVEL READ COMMITTED;
START TRANSACTION


testdb=# INSERT tbl(id, data)
VALUES (1,'phantom');
INSERT 1

testdb=# COMMIT;
COMMIT

事务B
testdb=# -- Tx_B: txid 101
testdb=# START TRANSACTION
testdb-# ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
testdb=# SELECT txid_current();
txid_current
--------------
101
(1 row)10
testdb=# SELECT * FROM tbl WHERE id=1;
id | data
----+------
(0 rows)