• [译] 5. PG并发控制


    [译] 5. 并发控制

    原文地址:https://www.interdb.jp/pg/pgsql05.html

    原文作者:Hironobu SUZUKI

    并发控制是一种在数据库中同时运行多个事务时保持 ACID 中原子性和隔离性两属性的机制。

    并发控制技术大致分为3类,即:多版本并发控制(Multi-version Concurrency Control - MVCC),严格2阶段锁(Strict Two-Phase Locking - S2PL)和乐观并发控制(Optimistic Concurrency Control - OCC),以及它们的各种变体。在MVCC中,每次写入操作都会创建数据的新版本并保留其旧版本。当一个事务读取一个数据项时,系统会选择其中一个版本来确保单个事务的隔离。MVCC的主要优点是“读不阻塞写,写不阻塞读”,相比之下,基于S2PL系统必须在写入时阻塞读,因为写操作需要获得其独占锁(exclusive lock)。PostgreSQL 和一些 RDBMS 使用一种称为快照隔离 (Snapshot Isolation - SI) 的 MVCC 变体。

    为了实现 SI,一些 RDBMS,例如 Oracle使用回滚段。当写入新数据时,其旧版本就被写入回滚段,随后新行以覆盖的方式写入到数据区。PostgreSQL 使用更简单的方式,新行被直接写入到相关的表页。当读数据时,PostgreSQL 使用可见性检查规则(visibility check rules)来选择一个合适的版本以响应单个事务。

    SI 隔离级别避免了ANSI-92标准中定义的三种并发问题:脏读(Dirty Reads),不可重复读(Non-Repeatable Reads)和幻读(Phantom Reads)。但是,它也无法实现真正的可串行化,因为它允许序列化异常,如:写偏斜(Write Skew) 和 只读事务倾斜(Read-only Transaction Skew)。请注意,基于经典可串行化定义的 ANSI SQL-92 标准并不等同于现代理论中的定义。为了解决这个问题,从 PG9.1 版本开始添加了可序列化快照隔离 (Serializable Snapshot Isolation - SSI)。SSI 可以检测到序列化异常,并可以解决由此类异常引起的冲突。因此,PostgreSQL 9.1 及更高版本提供了真正的 SERIALIZABLE 隔离级别。 (此外,SQL Server 也使用 SSI,Oracle 仍然只使用 SI。)

    本章包括以下四个部分内容:

    • 第1部分: 5.1~5.3节

      本部分提供理解后续部分所需的基本信息。

      第 5.1 节和第 5.2 节分别描述了事务 ID 和元组结构。 5.3 节展示了如何插入、删除和更新元组。

    • 第2部分:5.4~5.6 节

      这部分说明了实现并发控制机制所需的关键特性。

      第 5.4、5.5 和 5.6 节描述了提交日志 (clog),它分别保存所有事务状态、事务快照和可见性检查规则。

    • 第3部分:5.7~5.9节

      这部分通过具体的例子来描述 PostgreSQL 中的并发控制。

      第 5.7 节描述了可见性检查。本节还展示了如何防止 ANSI SQL 标准中定义的三个异常。第 5.8 节描述了防止丢失更新,第 5.9 节简要描述了 SSI 隔离。

    • 第4部分:5.10节

      这部分描述了持久运行并发控制机制所需的几个维护进程。第 6 章将介绍vacuum的处理过程。

    PostgreSQL 执行数据操作语言时使用SSI (Data Manipulation Language-DML)(例如 SELECT、UPDATE、INSERT、DELETE),将 两阶段锁(2PL) 用于 DDL(数据定义语言,例如 CREATE TABLE 等)。

    5.1 事务ID(Transaction ID)

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

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

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

    • 0:表示 Invalid txid
    • 1:表示 Bootstrap txid,仅用于初始化数据库集簇
    • 2:表示 Frozen txid,在5.10.1节介绍

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

    Fig. 5.1. Transaction ids in PostgreSQL.

    Fig. 5.1. Transaction ids in PostgreSQL.

    由于实际系统中 txid 范围可能不足,PostgreSQL 将 txid 视为一个圆圈。之前的 21 亿个 txid 是“过去的”,接下来的 21 亿个 txid 是“未来的”(图 5.1(b))。

    请注意,即第 5.10.1 节描述了所谓的 txid 环绕问题。

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

    5.2 元组结构

    表页中的堆元组分为普通数据元组和 TOAST 元组。本节仅描述普通数据元组。

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

    Fig. 5.2. Tuple structure

    Fig. 5.2. Tuple structure.

    HeapTupleHeaderData 结构在 src/include/access/htup_details.h 中定义。

    虽然 HeapTupleHeaderData 结构包含七个字段,但后续部分需要四个字段。

    • t_xmin :保存元组插入操作的事务的txid
    • t_xmax:保存元组更新或删除操作的事务的txid。如果此元组没有被删除或更新,t_xmax 设置为 0,这意味着 INVALID txid。
    • t_cid :保存插入命令的顺序ID(cid),表示本事务中从0开始到执行该命令前执行的SQL语句数量(即依次将命令从0开始编号,逐条命令递增)。例如,假设我们在单个事务中执行三个 INSERT 命令: 'BEGIN; INSERT; INSERT; INSERT; COMMIT;' 。如果在第一个insert命令插入元组,则t_cid 设置为0;第二个insert命令时,t_cid设置为1;依此类推。
    • t_ctid :保存指向自身或新元组的元组标识符(tid)。1.3节 介绍了tid用于标识表中的元组。当更新元组时,它(t_ctid)指向新元组的tid。否则,指向元组本身。实际上,t_ctid是一个二元组(x,y),其中x(从0开始编号)表示元组所在的page,y(从1开始编号)表示该page上的第几个位置

    5.3 插入,删除和更新元组

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

    为了聚焦在元组本身,下文未展现页头和行指针。图5.3 显示了元组的结构

    Fig. 5.3. Representation of tuples.

    5.3.1 插入

    使用插入操作将新元组直接写入到目标表的一个数据页中。(图5.4)

    Fig. 5.4. Tuple insertion.

    Fig. 5.4. Tuple insertion.

    假设有一个元组被一个txid为99的事务插入到一个数据页中。在这情况下,插入元组的页头字段设置如下:

    Tuple_1:

    • t_xmin 的值设为99,因为这个元组是由txid为99的事务执行的插入
    • t_xmax 设置为 0,因为该元组尚未被删除或更新
    • t_cid 设置为 0,因为这个元组是 txid 99 的第一个插入命令。
    • t_ctid 设置为 (0,1),指向自身,因为这是表的第一个元组,而它被放置在第0 page的第1个位置上
    pageinspect 扩展

    PostgreSQL 提供一个在contrib模块中名为 pageinspect 的扩展,它主要用于显示数据页的内容。

    testdb=# CREATE EXTENSION pageinspect;
    CREATE EXTENSION
    testdb=# CREATE TABLE tbl (data text);
    CREATE TABLE
    testdb=# INSERT INTO tbl VALUES('A');
    INSERT 0 1
    testdb=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid 
                 FROM heap_page_items(get_raw_page('tbl', 0));
    tuple | t_xmin | t_xmax | t_cid | t_ctid 
    -------+--------+--------+-------+--------
      1 |     99 |      0 |     0 | (0,1)
    (1 row)
    

    5.3.2 删除

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

    Fig. 5.5. Tuple deletion.

    Fig. 5.5. Tuple deletion.

    假设元组 Tuple_1 被 txid 为111的事务删除。此时 Tuple_1 的头部字段设置如下。

    Tuple_1:

    • t_xmax 被设置为111

    如果提交 txid 111 ,则不再需要元组Tuple_1。通常,在 PostgreSQL 中,将不需要的元组称为死元组(dead tuples)。

    死元组最终应该从页面中删除。而负责清理死元组的进程称为 VACUUM。其过程将在第六章介绍。

    5.3.3 更新

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

    Fig. 5.6. Update the row twice.

    Fig. 5.6. Update the row twice.

    假设存在被txid 99 插入的行,再被txid 100 更新了2次。

    在执行第1个UPDATE命令时,通过将 t_xmax 设置为100(txid)的方式逻辑元组Tuple_1,然后插入Tuple_2。然后,将元组 Tuple_1 的 t_ctid 重置为指向元组Tuple_2。Tuple_1 和 Tuple_2 的头字段如下所示。

    Tuple_1:

    • t_xmax 被设置为100
    • t_ctid 由(0,1)重写为(0,2)

    Tuple_2:

    • t_xmin 被设置为100

    • t_xmax 被设置为0

    • t_cid 被设置为0

    • t_ctid 被设置为(0,2)

    当执行第2条 UPDATE 命令时,和第1条更新一样,逻辑上删除 Tuple_2 并插入 Tuple_3。Tuple_2 和 Tuple_3 的头字段如下所示。

    Tuple_2:

    • t_xmax 被设置为100
    • t_ctid 由(0,2)重写为(0,3)

    Tuple_3:

    • t_xmin 被设置为100

    • t_xmax 被设置为0

    • t_cid 被设置为1

    • t_ctid 被设置为(0,3)

    和删除操作一样,如果txid 100提交成功,Tuple_1 和 Tuple_2 将变成死元组,否则,Tuple_2 和 Tuple_3 将变成死元组。

    5.3.4 空闲空间映射(Free Space Map)

    在插入一个堆元组或是索引元组时,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
    

    需要supper权限或pg_stat_scan_tables角色的用户可用

    5.4 提交日志(clog)

    PostgreSQL 在 Commit Log 中保存事务的状态。 Commit Log 又称为 clog,在共享内存中分配,并在整个事务处理中使用。

    本节介绍PostgreSQL 中事务的状态,clog 的运行方式和维护。

    5.4.1 事务状态

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

    前三种状态容易理解。例如,当事务正在进行时,其状态为 IN_PROGRESS 。

    而 SUB_COMMITTED 用于子事务,本文省略其描述。

    5.4.2 clog 工作过程

    clog 包含了共享内存中的一个或多个8KB页。clog 在逻辑上形成一个数组结构。数组下标是对应事务的ID,数组的值为该下标对应事务的状态。图5.7 显示了clog的结构。

    Fig. 5.7. How the clog operates

    Fig. 5.7. How the clog operates.

    时间线T1:txid 200 提交;其状态从 IN_PROGRESS 更改为 COMMITTED

    时间线T2:txid 201 异常退出;其状态从 IN_PROGRESS 更改为 ABORTED

    若当前的事务不断前行,而当前的clog page不足时,就在共享内存中追加一个新页。

    当需要时,可以调用内部函数获取事务的状态。这些函数读取clog并返回请求事务的状态。(参阅 5.7.1 节的 'Hint Bits')

    5.4.3 维护clog

    当 PostgreSQL 关闭或运行检查点时,将clog的数据写入到 pg_xact 子目录下文件中。(注意,在9.6或更早版本中 pg_xact 命名为 pg_clog)这些文件命名为 00000001 等。最大文件大小为256KB。例如:当clog使用了8页(从第1页到第8页面;总大小为$8 \times 8KB=64KB$),这些数据写入到 0000 (64KB)文件中;而使用37页($37 \times 8KB = 296KB$)时,数据写入到 0000 和 0001 文件(由于clog文件最大256KB,因此需要写入2文件),它们的大小分别为 256KB 和 40KB($296KB - 256KB = 40KB$)。

    当 PostgreSQL 启动时,存储在 pg_xact 文件中的数据被加载到共享内存用于初始化 clog。

    clog 的大小在不断增加,因为每当填满一个clog页时都会追加一个新页面。然而,备份所有的clog数据都是需要的。第6章介绍的 Vacuum进程会经常删除这些旧数据(包括clog 内存页和文件)。关于移除clog 数据的详情在第6.4节介绍。

    5.5 事务快照

    事务快照(transaction snapshot)是一个在某个时间点对单个事务来说,用于存储有关所有事务是否为活动状态信息的数据集。这里的活动事务表示它正在运行或未开始。

    在 PostgreSQL 内部将事务快照的文本表示格式定义为100:100:。例如:100:100: 表示“小于99 的txid是不活跃,等于或大于100的txid是活跃”。在下面的描述中,使用这种灵活的格式。如果不熟悉的话,可参阅下文。

    事务快照由事务管理器提供。在读已提交隔离级别下(READ COMMITTED),每当执行SQL命令时,事务都会获取快照;反之(REPEATABLE READ 或 SERIALIZABLE),事务仅在执行第一个 SQL 命令时获取快照。获取事务快照用于元组的可见性检查,详见5.7节。

    当使用可见性检查获取快照时,快照时的活动事务被视为正在运行中,即使它们实际上已提交或中止。此规则非常重要,因为它会导致READ COMMITTED 和 REPEATABLE READ(或 SERIALIZABLE)之间表现出不同的行为。我们在下面的章节中反复提及此规则。

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

    Fig. 5.9. Transaction manager and transactions.

    Fig. 5.9. Transaction manager and transactions.

    事务管理器始终保存有关当前正在运行的事务的信息。假设有三个事务依次启动,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 不可见。

    5.6 可见性检查规则(Visibility Check Rules)

    可见性检查规则是一组使用元组的t_xmin和t_xmax来确定每个元组在clog和获得事务快照中是可见还是不可见的规则。这些规则太复杂,无法详细解释。因此,本文档描述后续所需的最少规则。在下文中,我们省略了和子事务相关的规则,并忽略关于t_ctid 相关的讨论。即不考虑在事务中更新超过2次的元组。

    选择其中的10条规则,并分为三种情况。

    5.6.1 t_xmin 的状态为 ABORTED

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

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

    该规则明确表示为以下数学表达式:Rule 1: 如果 status(t_xmin) = ABORTED ⇒ Invisible

    5.6.2 t_xmin 的状态为 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 current 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

    5.6.3 t_xmin 的状态为 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 是 INVALID 或 ABORTED。三个例外条件以及规则 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

    5.7 可见性检查(Visibility Check)

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

    5.7.1 可见性检查

    图 5.10 显示了可见性检查的场景。

    Fig. 5.10. Scenario to describe visibility check.

    Fig. 5.10. Scenario to describe visibility check.

    在图 5.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 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 命令中,由规则8可知 Tuple_1 可见,由规则4可知 Tuple_2 不可见;因此,这个 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-conflicts)。但是,如上所述,在 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
      (1 row)
      

    请注意,在 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)
      

    5.7.2 在 PostgreSQL 的 REPEATABLE READ 级别中的幻读

    ANSI SQL-92 标准中定义的 REPEATABLE READ 允许幻读。但是,PostgreSQL 中的实现不允许幻读。原则上,SI 不允许幻读。

    假设 Tx_A 和 Tx_B 两个事务在同时运行。它们的隔离级别分别是 READ COMMITTED 和 REPEATABLE READ,txid 分别是 100 和 101。首先,Tx_A 插入一个元组并提交。插入元组的 t_xmin 是 100。接着,Tx_B 执行一个SELECT命令;但是,Tx_A 插入的元组,由规则 5 可知,在Tx_B 中是不可见的。因此,不会发生幻读。

    • Rule5(new tuple): Status(t_xmin:100) = COMMITTED ∧ Snapshot(t_xmin:100) = active ⇒ Invisible
    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
    
    
    testdb=# -- Tx_B: txid 101
    testdb=# START TRANSACTION
    testdb-#  ISOLATION LEVEL REPEATABLE READ;
    START TRANSACTION
    testdb=# SELECT txid_current();
     txid_current
    --------------
              101
    (1 row)
    
    testdb=# SELECT * FROM tbl WHERE id=1;
     id | data 
    ----+------
    (0 rows)
    

    5.8 防止丢失更新

    丢失更新也称为写写冲突 (ww-conflict),是在并发事务中更新相同行时发生的异常情况。在 REPEATABLE READ 和 SERIALIZABLE 隔离级别中必须防止发生这种异常。(注意,在READ COMMITTED级别不需要防止丢失更新)本节描述PostgreSQL如何防止丢失更新并举例说明。

    5.8.1 并发更新的行为

    当执行 UPDATE 命令时,在内部调用ExecUpdate函数。该函数的伪代码如下:

    ExecUpdate 函数伪代码

    (1)  FOR each row that will be updated by this UPDATE command
    (2)       WHILE true
    
                /* The First Block */
    (3)            IF the target row is being updated THEN
    (4)	              WAIT for the termination of the transaction that updated the target row
    
    (5)	              IF (the status of the terminated transaction is COMMITTED)
    	                   AND (the isolation level of this transaction is REPEATABLE READ or SERIALIZABLE) THEN
    (6)	                       ABORT this transaction  /* First-Updater-Win */
    	              ELSE 
    (7)                           GOTO step (2)
    	              END IF
    
                /* The Second Block */
    (8)            ELSE IF the target row has been updated by another concurrent transaction THEN
    (9)	              IF (the isolation level of this transaction is READ COMMITTED THEN
    (10)	                       UPDATE the target row
    	              ELSE
    (11)	                       ABORT this transaction  /* First-Updater-Win */
    	              END IF
    
                /* The Third Block */
                 ELSE  /* The target row is not yet modified or has been updated by a terminated transaction. */
    (12)	              UPDATE the target row
                 END IF
            END WHILE 
       END FOR 
    

    (1) 获取将由UPDATA命令更新的每一行

    (2) 重复以下过程,直到目标行被更新(或该事务被中止)。

    (3) 如果目标行正在更新,则进行步骤(3);否则,进行步骤(8)

    (4) 等待更新目标行的事务终止,因为 PostgreSQL 在 SI 中使用 first-updater-win 方案

    (5) 如果更新目标行的事务状态为COMMITTED,且该事务的隔离级别为REPEATABLE READ(或SERIALIZABLE),则继续执行步骤(6);否则,进行步骤(7)

    (6) 中止此事务以防止丢失更新

    (7) 进行步骤(2),尝试在下一轮更新目标行

    (8) 如果目标行已被另一个并发事务更新,则进行步骤(9);否则,进行步骤(12)

    (9) 如果该事务的隔离级别为READ COMMITTED,则进行步骤(10);否则,进行步骤(11)

    (10) 更新目标行,并进行步骤(1)

    (11) 中止此事务以防止丢失更新

    (12) 更新目标行并继续执行步骤(1),因为目标行尚未被修改或已被终止的事务更新,即存在ww-conflict

    此函数对每一个目标行执行更新操作。它使用While循环更新每一行,其循环内部根据图5.11所示的条件分成3个分支。

    Fig. 5.11. Three internal blocks in ExecUpdate.

    Fig. 5.11. Three internal blocks in ExecUpdate.

    [1] 正更新目标行(图5.11[1])

    ​ 'Being updated' 意味着该行被另一个并发事务更新其事务尚未终止。在这种情况下,当前事务必须等待更新目标行的事务终止。因为 PostgreSQL 的SI隔离级别使用 first-updater-win 方案。例如,假设事务 Tx_A 和 Tx_B 并发运行,并且 Tx_B 尝试更新一行;但是,Tx_A 已对其进行了更新,并且仍在进行状态。此刻,Tx_B 等待 Tx_A 的终止。

    ​ 更新目标行的事务(Tx_A)提交后,继续当前事务(Tx_B)的更新操作。如果当前事务处于 READ COMMITTED 级别,则更新目标行;否则(REPEATABLE READ 或 SERIALIZABLE),当前事务立即中止以防止丢失更新。

    [2] 目标行已被并发事务更新(图 5.11[2])

    ​ 当前事务尝试更新目标元组;但是,另一个并发事务已更新目标行并已提交。在这种情况下,如果当前事务处于 READ COMMITTED 级别,则将更新目标行;否则,当前事务将立即中止以防止丢失更新。

    [3] 不存在冲突(图 5.11[3])

    ​ 当没有冲突时,当前事务可以更新目标行。

    first-updater-win/ first-commiter-win

    如本节所述,PostgreSQL 基于 SI 的并发控制使用 first-updater-win 的方案来避免丢失更新异常。相反,正如下一节的介绍,PostgreSQL 的 SSI 使用 first-committer-win 方案来避免序列化异常。

    5.8.2 示例

    下面显示了三个示例。第一个和第二个示例显示being updated目标行时的行为,第三个示例显示已更新目标行时的行为。

    示例1

    事务 Tx_A 和 Tx_B 更新同一张表中的同一行,它们的隔离级别为 READ COMMITTED。

    testdb=# -- Tx_A
    testdb=# START TRANSACTION
    testdb-#    ISOLATION LEVEL READ COMMITTED;
    START TRANSACTION
    
    
    testdb=# UPDATE tbl SET name = 'Hyde';
    UPDATE 1
    
    
    
    testdb=# COMMIT;
    COMMIT
    
    testdb=# -- Tx_B
    testdb=# START TRANSACTION
    testdb-#    ISOLATION LEVEL READ COMMITTED;
    START TRANSACTION
    
    
    testdb=# UPDATE tbl SET name = 'Utterson';
        ↓ 
        ↓ this transaction is being blocked
        ↓ 
    UPDATE 1
    

    image-20220321103842151

    Tx_B 按如下执行名:

    1. 执行UPDATE命令后,Tx_B应该等待Tx_A的终止,因为目标元组正在被Tx_A更新(ExecUpdate函数中的步骤(4))
    2. Tx_A 提交后,Tx_B 尝试更新目标行(ExecUpdate 中的步骤(7))
    3. 3)在第二轮ExecUpdate中,目标行被Tx_B再次更新(ExecUpdate中的步骤(2),(8),(9),(10))
    示例2

    事务 Tx_A 和 Tx_B 更新同一张表中的同一行,它们的隔离级别分别是 READ COMMITTED 和 REPEATABLE READ。

    testdb=# -- Tx_A
    testdb=# START TRANSACTION
    testdb-#    ISOLATION LEVEL READ COMMITTED;
    START TRANSACTION
    
    
    testdb=# UPDATE tbl SET name = 'Hyde';
    UPDATE 1
    
    
    
    testdb=# COMMIT;
    COMMIT
    
    
    -- --------------------
    
    
    testdb=# -- Tx_B
    testdb=# START TRANSACTION
    testdb-#    ISOLATION LEVEL REPEATABLE READ;
    START TRANSACTION
    
    
    testdb=# UPDATE tbl SET name = 'Utterson';
        ↓ 
        ↓ this transaction is being blocked
        ↓
    ERROR:couldn't serialize access due to concurrent update
    
    

    image-20220321105325260

    Tx_B 的行为如下所述:

    1. 执行UPDATE命令后,Tx_B应该等待Tx_A的终止(ExecUpdate函数中的步骤(4))
      1. Tx_A 提交后,Tx_B 被中止以解决冲突,因为目标行已经更新,并且该事务的隔离级别为 REPEATABLE READ(ExecUpdate 中的步骤(5),(6))
    示例3

    Tx_B (REPEATABLE READ) 尝试更新已被 Tx_A 更新提交的目标行。在这种情况下,Tx_B 被中止(ExecUpdate 中的步骤 (2)、(8)、(9) 和 (11))。

    testdb=# -- Tx_A
    testdb=# START TRANSACTION
    testdb-#    ISOLATION LEVEL READ COMMITTED;
    START TRANSACTION
    
    
    testdb=# UPDATE tbl SET name = 'Hyde';
    UPDATE 1
    
    
    testdb=# COMMIT;
    COMMIT
    
    
    -- --------------------------
    
    
    testdb=# -- Tx_B
    testdb=# START TRANSACTION
    testdb-#    ISOLATION LEVEL REPEATABLE READ;
    START TRANSACTION
    testdb=# SELECT * FROM tbl;
      name  
    --------
     Jekyll
    (1 row)
    testdb=# UPDATE tbl SET name = 'Utterson';
    ERROR:couldn't serialize access due to concurrent update
    

    image-20220321105931332

    5.9 序列化快照隔离(Serializable Snapshot Isolation)

    Serializable Snapshot Isolation(SSI) 自9.1版本以来嵌入到SI中,实现了正在的 SERIALIZABLE 隔离级别。 由于 SSI 不是一两句就能说清楚的,这里只介绍一个大纲。详细参考2

    在下文中,将直接使用下面的专业术语,若不熟悉,请参阅13

    • precedence graph优先图 (also known as dependency graph and serialization graph)
    • serialization anomalies (e.g. Write-Skew)

    5.9.1 SSI 实现的基本策略

    如果在优先图中存在产生一些冲突的循环,就会出现串行化异常。用最简单的异常来说明就是写偏斜(write-skew)。

    图5.12(1)显示了一个计划表。Transaction_A 读取 Tuple_B,Transaction_B 读取 Tuple_A。然后,Transaction_A 写入 Tuple_A,Transaction_B 写入 Tuple_B。在这种情况下,有2个rw-conflicts,并且形成了一个调度优先图的循环,如图5.12(2)所示。因此,这种调度存在一个串行化异常,即write-skew。

    Fig. 5.12. Write-Skew schedule and its precedence graph.

    Fig. 5.12. Write-Skew schedule and its precedence graph.

    从概念上讲,存在3种类型的冲突: wr-conflicts(脏读),ww-conflicts(丢失更新)和rw-conflicts。但是,不需要考虑 wr-conflicts 和 ww-conflicts 。就如前几节所述,PostgreSQL 已避免发生这些冲突。因此,在 PostgreSQL 中实现 SSI 只需要考虑 rw-conflicts 即可。

    PostgreSQL 在实现 SSI 中采取以下策略:

    1. 在事务中访问所有对象(元组,页面,关系)记录加 SIREAD 锁。
    2. 每当写入任何堆或索引元组时,使用 SIREAD 锁检测 rw-conflicts。
    3. 如果 rw-conflicts 检测时遇到序列化异常,则中止事务。

    5.9.2 在 PostgreSQL 中实现SSI

    为了实现上述策略,PostgreSQL 实现了许多功能和数据结构。但是,这里我们只使用了两种数据结构:SIREAD 锁和 rw-conflicts 来描述 SSI 机制。它们存储在共享内存中。

    为简单起见,本文档中省略了一些重要的数据结构的说明,例如 SERIALIZABLEXACT。因此,对CheckTargetForConflictOut、CheckTargetForConflictIn、PreCommit_CheckForSerializationFailure等函数的介绍也极为简化。例如,我们指出了哪些函数用于检测冲突;但是,没有详细介绍如何检测冲突。如果想了解详情,请参考源码:predicate.c。

    SIREAD locks

    SIREAD 锁,在内部也称为 predicate lock。它由一个对象和(虚拟)txid组成二元组(x,y),用于存储对象访问相关的信息。注意,这里省略了虚拟txid(virtual txid)的描述。使用txid而不是virtual txid来简化下文说明。

    在 SERIALIZABLE 模式中,每当执行一条 DML 命令时就会通过CheckTargetForConflictsOut 函数创建SIREAD 锁。例如:如果 txid 100 读取给定表中的 Tuple_1,就创建了一个 SIREAD 锁 {Tuple_1, {100}}。如果另一个事务,如 txid 101 也读 Tuple_1,那么SIREAD 锁更新为{Tuple_1, {100,101}}。请注意,在读取索引页时也会创建 SIREAD 锁。因为使用 Index-Only Scans访问时只需读取索引页而不用回表,详细内容参阅第7.2节内容。

    SIREAD 锁分成3个级别:元组(tuple),页(page)和关系(relation)。如果创建了单个页内所有元组的 SIREAD 锁,则将它们聚合为该页面的单个SIREAD 锁,并将释放(移除)相关元组的所有SIREAD 锁,以减少内存空间。对于读取所有页面也是同理。

    每当为索引创建 SIREAD 锁时,从一开始就创建页级别的SIREAD 锁。当使用顺序扫描时,无论是否存在索引或Where 子句,都会从一开始就创建一个relation级别的SIREAD 锁。请注意,在某些场景中,此实现策略可能会出现串行化异常监测的误报。详情在第5.9.4节描述。

    rw-conflicts

    rw-conflict 是一个 SIREAD 锁 和两个读写SIREAD 锁的txid 的三元组。

    每当在SERIALIZABLE 模式下执行 INSERT, UPDATE 和 DELETE 命令时,都会调用CheckTargetForConflictsIn 函数,并通过检查 SIREAD 锁冲突时创建rw-conflict。

    例如,假设 txid 100 读取 Tuple_1,然后 txid 101 更新 Tuple_1。在这种情况下,由 txid 101 中的 UPDATE 命令调用的 CheckTargetForConflictsIn 函数检测到 txid 100 和 101 之间的 Tuple_1 的 rw-conflict ,然后创建 rw-conflict {r=100, w=101, {Tuple_1}}。

    CheckTargetForConflictOut 和 CheckTargetForConflictIn 函数,以及在 SERIALIZABLE 模式下执行 COMMIT 命令时调用的 PreCommit_CheckForSerializationFailure 函数,都使用创建的 rw-conflicts 检查序列化异常。如果他们检测到异常,只有第一个提交的事务被提交,其他事务被中止(通过 first-committer-win 方案)。

    5.9.3 SSI 处理过程

    在这里,我们描述了 SSI 如何解决 Write-Skew 异常。我们使用一张简单表 tbl如下:

    testdb=# CREATE TABLE tbl (id INT primary key, flag bool DEFAULT false);
    testdb=# INSERT INTO tbl (id) SELECT generate_series(1,2000);
    testdb=# ANALYZE tbl;
    

    事务 Tx_A 和 Tx_B 执行以下命令(图 5.13)

    Fig. 5.13. Write-Skew scenario.

    Fig. 5.13. Write-Skew scenario.

    假设所有命令都使用索引扫描。因此,在执行命令时,它们会同时读取堆元组和索引页,其中每一个都包含指向相应堆元组的索引元组。参见图 5.14。

    Fig. 5.14. Relationship between the index and table in the scenario shown in Fig. 5.13.

    • T1: Tx_A 执行一个SELECT命令。该命令读取一个堆元组(Tuple_2000)和一个主键(Pkey_2)索引页
    • T2:Tx_B 执行一个SELECT命令。该命令读取一个堆元组(Tuple_1)和一个主键(Pkey_1)索引页
    • T3:Tx_A 执行一个UPDATE命令更新Tuple_1
    • T4:Tx_B 执行一个UPDATE命令更新Tuple_2000
    • T5:Tx_A 提交
    • T6:Tx_B 提交。但是,由于 write-skew 异常而中止。

    图 5.15 显示了 PostgreSQL 如何检测和解决上述场景中描述的 Write-Skew 异常。

    Fig. 5.15. SIREAD locks and rw-conflicts, and schedule of the scenario shown in Fig. 5.13.

    • T1
      • 当Tx_A执行SELECT命令时,CheckTargetForConflictsOut 函数创建SIREAD 锁。在此例中,该函数创建了2个SIREAD 锁:L1和L2。
      • L1 和 L2 分别与 Pkey_2 和 Tuple_2000 相关联。
    • T2
      • 当Tx_B执行SELECT命令时,CheckTargetForConflictsOut 函数创建SIREAD 锁。在此例中,该函数创建了2个SIREAD 锁:L3和L4。
      • L3 和 L4 分别与 Pkey_1 和 Tuple_1 相关联。
    • T3
      • 当Tx_A执行UPDATE 命令时,在 ExecUpdate 之前和之后都会调用 CheckTargetForConflictsOut 和 CheckTargetForConflictsIN 函数
      • 在这种情况下,CheckTargetForConflictsOut 什么都不做。
      • CheckTargetForConflictsIn 创建 rw-conflict C1,即 Tx_B 和 Tx_A 之间的 Pkey_1 和 Tuple_1 的冲突,因为 Pkey_1 和 Tuple_1 都是由 Tx_B 读取并由 Tx_A 写入的。
    • T4
      • 当Tx_B执行UPDATE 命令时,CheckTargetForConflictsIn 创建 rw-conflict C2。即 Tx_B 和 Tx_A 之间的 Pkey_2 和 Tuple_2000 的冲突.
      • 在这种情况下,C1 和 C2 在优先图中创建一个循环;因此,Tx_A 和 Tx_B 处于不可串行化状态。但是,事务 Tx_A 和 Tx_B 都没有提交,因此 CheckTargetForConflictsIn 不会中止 Tx_B。请注意,这是因为 PostgreSQL 的 SSI 实现基于 first-committer-win 方案。
    • T5
      • 当 Tx_A 尝试提交时,会调用 PreCommit_CheckForSerializationFailure。此函数可以检测序列化异常,并在可能的情况下执行提交操作。在这种情况下,由于 Tx_B 仍在进行中,因此提交了 Tx_A。
    • T6
      • 当 Tx_B 尝试提交时,PreCommit_CheckForSerializationFailure 检测到序列化异常并且 Tx_A 已经提交;因此,Tx_B 被中止。

    此外,如果在提交 Tx_A 之后(在T5时刻)由 Tx_B 执行 UPDATE 命令,则 Tx_B 立即中止,因为 Tx_B 的 UPDATE 命令调用的 CheckTargetForConflictsIn 检测到序列化异常(图 5.16(1))。

    如果在 T6 执行 SELECT 命令而不是 COMMIT,则 Tx_B 立即中止,因为 Tx_B 的 SELECT 命令调用的 CheckTargetForConflictsOut 检测到序列化异常(图 5.16(2))。

    Fig. 5.16. Other Write-Skew scenarios.

    Fig. 5.16. Other Write-Skew scenarios.

    5.9.4 序列化异常误报

    在 SERIALIZABLE 模式下,始终可以保证并发事务的串行化,因为永远不会检测到假阴性序列化异常。但是,在某些情况下,可以检测到异常误报。因此,用户在使用 SERIALIZABLE 模式时应牢记这一点。下面介绍 PostgreSQL 检测到误报异常的情况。

    图 5.17 显示了发生误报序列化异常的场景。

    Fig. 5.17. Scenario where false-positive serialization anomaly occurs.

    Fig. 5.17. Scenario where false-positive serialization anomaly occurs.

    在使用顺序扫描时,正如在描述 SIREAD 锁中提到的,PostgreSQL 会创建一个ralation级别的 SIREAD 锁。图 5.18(1) 显示了 PostgreSQL 使用顺序扫描时的 SIREAD 锁和 rw-conflicts。在这种情况下,与 tbl 的 SIREAD 锁相关联的 rw-conflicts C1 和 C2 被创建,它们在优先图中构成了一个循环。因此,检测到假阳性write-skew异常(即使没有冲突,Tx_A 或 Tx_B 也将被中止)。

    Fig. 5.18. False-positive anomaly (1) – Using sequential scan.

    Fig. 5.18. False-positive anomaly (1) – Using sequential scan.

    即使使用索引扫描,如果事务 Tx_A 和 Tx_B 都获得相同的索引 SIREAD 锁,PostgreSQL 也会检测到异常误报。图 5.19 显示了这种情况。假设索引页Pkey_1包含两个索引项,一个指向Tuple_1,另一个指向Tuple_2。当 Tx_A 和 Tx_B 执行各自的 SELECT 和 UPDATE 命令时,Tx_A 和 Tx_B 都会读取和写入 Pkey_1。在这种情况下,rw-conflicts C1 和 C2 都与 Pkey_1 相关联,构成了一个循环优先图;因此,检测到写入偏斜异常误报。 (如果 Tx_A 和 Tx_B 获得不同索引页的 SIREAD 锁,则不会检测到误报,两个事务都可以提交。)

    Fig. 5.19. False-positive anomaly (2) – Index scan using the same index page.

    Fig. 5.19. False-positive anomaly (2) – Index scan using the same index page.

    5.10 所需的维护过程

    PostgreSQL 的并发控制机制需要以下维护过程:

    1. 删除死元组和指向死元组的索引元组
    2. 移除clog中不需要的部分
    3. 冻结旧txid
    4. 更新 FSM(空闲空间映射)、VM(可见性映射) 和统计信息

    第 5.3.2 节和第 5.4.3 节分别解释了第一个和第二个过程的必要性。第三个过程与transaction id wraparound(事务ID环绕)问题有关,下面小节将简要介绍。

    在 PostgreSQL 中,VACUUM 负责这些处理过程,它在第 6 章中描述。

    5.10.1 冻结处理

    在这里,我们描述了 txid 环绕问题。

    假设插入元组Tuple_1的 txid 为 100,即 Tuple_1 的 t_xmin 为 100。系统运行了很长时间,Tuple_1未被修改。当前 txid 为 21 亿 + 100 并且执行了一个 SELECT 命令。此刻,Tuple_1 是可见的,因为 txid 100是过去式。然后,执行相同的 SELECT 命令;因此,当前的txid是21亿 + 101。然而,Tuple_1 不再可见,因为 txid 100 在未来(图 5.20)这就是 PostgreSQL 中所谓的事务回绕问题。

    Fig. 5.20. Wraparound problem.

    Fig. 5.20. Wraparound problem.

    为了解决这个问题,PostgreSQL 引入了一个叫做 freeze txid 的概念,并实现了一个叫做 FREEZE 的进程。

    在 PostgreSQL 中,一个冻结的 txid 是一个特殊的保留的,其值为2。它被定义为总是比所有其它 txid 更老。换句话说,冻结的 txid 始终处于非活动状态且可见。

    冻结过程由vacuum进程调用。 冻结进程扫描所有表文件,如果 t_xmin 值比当前 txid 减去 vacuum_freeze_min_age(默认为 5000 万)旧,则将 t_xmin 的元组重写为冻结的 txid。这在第 6 章中有更详细的解释。

    例如,如图 5.21 a) 所示,当前 txid 为 5000 万,冻结过程由 VACUUM 命令调用。在这种情况下,Tuple_1 和 Tuple_2 的 t_xmin 都被重写为 2。

    在 9.4 或更高版本中,XMIN_FROZEN 位设置为元组的 t_infomask 字段,而不是将元组的 t_xmin 重写为冻结的 txid(图 5.21 b)。

    Fig. 5.21. Freeze process.

    Fig. 5.21. Freeze process.

    附录

    PostgreSQL 中的事务隔离级别

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

    Isolation Level Dirty Reads(脏读) Non-repeatable Read(不可重复读) Phantom Read(幻读) Serialization Anomaly(串行化异常)
    READ COMMITTED Not possible Possible Possible Possible
    REPEATABLE READ$^*1$ Not possible Not possible Not possible in PG; See Section 5.7.2. (Possible in ANSI SQL) Possible
    SERIALIZABLE Not possible Not possible Not possible Not possible

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

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

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

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

    txid_current_snapshot 的文本表示格式为‘xmin:xmax:xip_list’,其各部分字段说明如下:

    • xmin: 仍处于活动状态的最早的txid。即所有早期的事务要么是被提交且可见的,要么是回滚且kill掉。
    • xmax: 第一个尚未分配的txid。所有大于或等于的txid在此快照时间尚未启动,因此是不可见的。
    • xip_list: 快照时的活动 txid。该列表仅包括 xmin 和 xmax 之间的活动 txid。

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

    下面给出2个具体的例子:

    Fig. 5.8. Examples of transaction snapshot representation.

    Fig. 5.8. Examples of transaction snapshot representation.

    第一个例子是'100: 100:'。该快照的含义如下(图 5.8(a)):

    • 等于或小于 99 的 txid 是不活动的事务,因为 xmin 为 100。
    • 等于或大于 100 的 txid 处于活动状态,因为 xmax 为 100。

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

    • 等于或小于 99 的 txid 是不活动的事务
    • 等于或大于 104 的 txid 处于活动状态
    • txid 100 和 txid 102是活动的,因为它们处于xip_list中,而 txid 101 和 103 是非活动状态。

    Hint Bits

    为了获取事务的状态,PostgreSQL 内部提供了三个函数,即 TransactionIdIsInProgress、TransactionIdDidCommit 和 TransactionIdDidAbort。实现这些功能是为了减少对 clog 的频繁访问,例如缓存。但是,如果在检查每个元组时都执行它们,则会出现瓶颈。

    为了解决这个问题,PostgreSQL 使用了 hint bits,如下所示。

    #define HEAP_XMIN_COMMITTED       0x0100   /* t_xmin committed */
    #define HEAP_XMIN_INVALID         0x0200   /* t_xmin invalid/aborted */
    #define HEAP_XMAX_COMMITTED       0x0400   /* t_xmax committed */
    #define HEAP_XMAX_INVALID         0x0800   /* t_xmax invalid/aborted */
    

    在读取或写入元组时,PostgreSQL 会尽可能将hint bits设置为元组的 t_informask。例如,假设 PostgreSQL 检查一个元组的 t_xmin 的状态并获得 COMMITTED 状态。在这种情况下,PostgreSQL 将 HEAP_XMIN_COMMITTED 设置为元组的 t_infomask。如果hint bits已设置,则不再需要设置 TransactionIdDidCommit 和 TransactionIdDidAbort。因此,PostgreSQL 可以有效地检查每个元组的 t_xmin 和 t_xmax 的状态。

  • 相关阅读:
    linux静态链接库
    查看进程运行时间
    进程间同步-互斥量
    Linux——多线程下解决生产消费者模型
    Linux——线程
    浅谈智能指针的历史包袱
    C++ 模板基础
    用信号量为共享内存添加同步机制
    Linux——浅析信号处理
    浅析fork()和底层实现
  • 原文地址:https://www.cnblogs.com/binliubiao/p/16037397.html
Copyright © 2020-2023  润新知