Postgresql管理系列-第五章 并发控制

当数据库中多个事务并发运行时,并发控制机制用于保持一致性和隔离性,这两个特性是ACID的两个属性。

有三种广泛的并发控制技术,即多版本并发控制(MVCC),严格两阶段锁(S2PL),和乐观并发控制(OCC),每个技术又有许多变体。在MVCC,每个写操作会创建一个新版本的数据项,同时保留旧版本。当一个事务读取数据项,系统选择一个版本,以确保事物之间的隔离性。MVCC的主要优势是,“读不会阻塞写,写也不会阻塞读”。对比之下,例如,一个基于S2PL系统在写操作发生时会阻塞该对象上的读操作,因为该对象获得独占锁。PostgreSQL和一些rdbms数据库使用变体的MVCC称为快照隔离(SI)。

一些关系型数据库,例如,oracle,使用回滚段实现了SI技术。在写入一个新的数据项时,旧版本的项目写入回滚段,随后新项目重写至数据区域。PostgreSQL使用更简单的方法。新的数据项直接插入相关的表页中。当读取项目时,PostgreSQL根据可视化检查规则,为每个数据项选择合适的对象版本响应请求。

在ANSI sql - 92标准中,SI不允许的三个异常定义,即脏读、不可重复读和幻读。然而,SI不能达到真正的可串行化,因为SI会出现序列化异常,如写倾斜和只读事务倾斜。注意,ANSI sql - 92基于标准的可串行性定义的定义并不等同于现代理论的定义。为了解决这个问题,可序列化的快照隔离(SSI) 9.1版本添加了。SSI可以检测序列化异常,并解决异常引起的冲突。因此,PostgreSQL 9.1之后提供了一个真正的可序列化的隔离级别。(此外,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节。
本部分描述了长期运行并发控制机制所需的几个维护过程。维护过程通过 vacuum processing处理进行,如第6章所述。

本章重点介绍PostgreSQL的主题,尽管有许多与并发控制相关的主题。请注意,这里省略了死锁的预防和锁定模式的描述(有关更多信息,请参阅官方文档)。

Postgresql中的事物隔离级别
名词翻译:
Dirty Reads 脏读(可见其他未提交事务的数据)
Non-repeatable Read 不可重复读 (重复执行语句后,可以读取修改后的数据)
Phantom Read 幻读 (重复执行语句后,可以读取到新增或者删除的数据)
Serialization Anomaly 序列化异常
在这里插入图片描述
PostgreSQL使用SSI用于DML(数据操作语言,例如,SELECT,UPDATE,INSERT,DELETE),以及2PL用于DDL(数据定义语言,例如,CREATE TABLE等)。

5.1 事物ID

每当事务开始时,事务管理器就会分配一个称为事务id(txid)的唯一标识符。 PostgreSQL的txid是一个32位无符号整数,大约42亿。如果在事务启动后执行内置的txid_current()函数,则该函数将返回当前的txid,如下所示。
在这里插入图片描述
PostgreSQL保留以下三个特殊的txid:

0表示无效的txid。
1表示Bootstrap txid,仅用于数据库集群的初始化。
2表示冻结txid,如第5.10.1节所述。
Txids可以相互比较。例如,从txid =100的角度去看时候,大于100的txids是“未来”,并且它们从txid =100以后是不可见的;小于100的txids是“过去”并且可见的(图5.1a))。

图5.1 Postgresql中的事物ID
在这里插入图片描述
由于实际系统中的txid空间不足,PostgreSQL将txid空间视为一个圆环。之前的21亿txids是’过去’,接下来的21亿txids是’未来’(图5.1b)。

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

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

5.2 元组结构

表页中的堆元组被分类为普通的的数据元组和TOAST元组。本节仅介绍普通元组。
堆元组包括三个部分,即HeapTupleHeaderData结构,NULL位图和用户数据(图5.2)。
图5.2 元组结构
在这里插入图片描述

HeapTupleHeaderData定义在src/include/access/htup_details.h.

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

t_xmin保存插入此元组的事务的txid。
t_xmax保存删除或更新此元组的事务的txid。如果此元组尚未删除或更新,则t_xmax设置为0,意思是INVALID。
t_cid保存命令id(cid),这意味着在从0开始的当前事务中执行此命令之前执行了多少SQL命令。例如,假设我们在单个事务中执行三个INSERT命令:‘BEGIN;INSERT;INSERT;INSERT;COMMIT;’。如果第一个命令插入此元组,则t_cid设置为0.如果第二个命令插入此命令,则t_cid设置为1,依此类推。
t_ctid保存指向自身或新元组的元组标识符(tid)。第1.3节中描述的tid用于标识表中的元组。更新此元组时,此元组的t_ctid指向新元组;否则,t_ctid指向自己。

HeapTupleHeaderData的结构定义:

typedef struct HeapTupleFields
{
        TransactionId t_xmin;		   /* inserting xact ID */
        TransactionId t_xmax;              /* deleting or locking xact ID */

        union
        {
                CommandId       t_cid;     /* inserting or deleting command ID, or both */
                TransactionId 	t_xvac;    /* old-style VACUUM FULL xact ID */
        } t_field3;
} HeapTupleFields;

typedef struct DatumTupleFields
{
        int32          datum_len_;          /* varlena header (do not touch directly!) */
        int32          datum_typmod;   	    /* -1, or identifier of a record type */
        Oid            datum_typeid;   	    /* composite type OID, or RECORDOID */

        /*                                                                                               
         * Note: field ordering is chosen with thought that Oid might someday                            
         * widen to 64 bits.                                                                             
         */
} DatumTupleFields;

typedef struct HeapTupleHeaderData
{
        union
        {
                HeapTupleFields t_heap;
                DatumTupleFields t_datum;
        } t_choice;

        ItemPointerData t_ctid;         /* current TID of this or newer tuple */

        /* Fields below here must match MinimalTupleData! */
        uint16          t_infomask2;    /* number of attributes + various flags */
        uint16          t_infomask;     /* various flag bits, see below */
        uint8           t_hoff;         /* sizeof header incl. bitmap, padding */
        /* ^ - 23 bytes - ^ */
        bits8           t_bits[1];      /* bitmap of NULLs -- VARIABLE LENGTH */

        /* MORE DATA FOLLOWS AT END OF STRUCT */
} HeapTupleHeaderData;

typedef HeapTupleHeaderData *HeapTupleHeader;
5.3 插入,删除和更新元组

本节介绍如何插入,删除和更新元组。然后,简要描述用于插入和更新元组的空闲空间映射(FSM)。
元组,页头和行指针不会在下面描述。图5.3显示了如何表示元组的示例。

图5.3 元组的描述

5.3.1 插入

通过插入操作,新的元组将直接插入目标表的页面中(图5.4)。

图5.4 元组插入
在这里插入图片描述
假设由txid为99的事务在页面中插入元组。在这种情况下,插入元组的头字段设置如下。

Tuple_1:
t_xmin设置为99,因为此元组由txid 99插入。
t_xmax设置为0,因为此元组尚未删除或更新。
t_cid设置为0,因为此元组是txid 99插入的第一个元组。
t_ctid设置为(0,1),指向自身,因为这是最新的元组。

PostgreSQL提供了一个插件pageinspect,用于显示数据库页面的内容。
在这里插入图片描述

5.3.2 删除

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

图5.5 元组删除
在这里插入图片描述
假设Tuple_1被txid 111删除。在这种情况下,Tuple_1的头字段设置如下。

Tuple_1:
t_xmax设置为111。
如果提交了txid 111,则不再需要Tuple_1。通常,不需要的元组在PostgreSQL中称为死元组。
应该最终从页面中删除死元组。清除死元组称为VACUUM处理,将在第6章中介绍。

5.3.3 更新

在更新操作中,PostgreSQL在逻辑上删除最新的元组并插入一个新的元组(图5.6)。
图 5.6 两次更新
在这里插入图片描述
假设由txid 99插入的行由txid 100更新两次。

当执行第一个UPDATE命令时,通过将txid 100设置为t_xmax来逻辑删除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)。

当执行第二个UPDATE命令时,如在第一个UPDATE命令中,逻辑删除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将是死元组,并且如果txid 100被中止,则Tuple_2和Tuple_3将是死元组。

5.3.4 空闲空间映射

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

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

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

pg_freespacemap
插件pg_freespacemap提供指定表/索引的空闲空间。以下查询显示指定表中每个页面的空闲空间比率。
在这里插入图片描述

5.4 Commit Log (clog)

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

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

5.4.1 事物状态

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

前三种状态很明显和字面意思一样。例如,当事务正在进行时,其状态为IN_PROGRESS等。

SUB_COMMITTED用于子事务,本文档中省略其描述

5.4.2 clog如何执行

clog包含共享内存中的一个或多个8 KB页面。 clog逻辑上是一个数组。数组的索引对应于相应的事务id,并且数组中的每个项都保存相应事务id的状态。图5.7显示了blog及其运行方式。

在这里插入图片描述

T1: txid 200 commits; txid 200 的状态由IN_PROGRESS改变为 COMMITTED.
T2: txid 201 aborts; txid 201 的状态由IN_PROGRESS改变为ABORTED.

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

当需要事务的状态时,将调用内部函数。这些函数读取clog并返回所请求事务的状态。 (另请参见第5.7.1节中的“Hint Bits”。)

5.4.3 clog维护

当PostgreSQL关闭或检查点进程运行时,clog的数据将写入存储在pg_clog子目录下的文件中。 (请注意,在版本10中,pg_clog将重命名为pg_xact。)这些文件名为0000,0001等。最大文件尺寸为256 KB。例如,当clog使用八个页面(第一页到第八页;总大小为64 KB)时,其数据写入0000(64 KB),当有37页(296 KB)时,数据写入到0000和0001,其大小分别为256 KB和40 KB。

当PostgreSQL启动时,加载存储在pg_clog文件(pg_xact的文件)中的数据以初始化clog。

clog的大小不断增加,因为只要填充了clog就追加新页面。但是,clog中并非所有数据都是必要的。第6章中描述的vacuum process定期删除这些旧数据(clog页面和文件)。有关删除clog数据的详细信息,请参见第6.4节。

5.5 事物快照

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

PostgreSQL内部将事务快照的文本表示格式定义为“100:100:”。例如,'100:100:‘表示’小于99的txids未激活,并且等于或大于100的txids处于活动状态’。在以下描述中,使用这种方便的表示形式。如果您不熟悉它,请参阅下文

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

函数txid_current_snapshot显示当前事务的快照。
在这里插入图片描述
txid_current_snapshot的文本表示是’xmin:xmax:xip_list’,组件描述如下。

  • XMIN
    最早的txid仍然活跃。所有早期的事务都将被提交并且可见,或者回滚并且死亡。
  • XMAX
    第一个尚未分配的txid。所有大于或等于此的txids在快照时尚未启动,因此不可见。
  • xip_list
    快照时的活动txids。该列表仅包括xmin和xmax之间的活动txid。
    例如,在快照’100:104:100,102’中,xmin是’100’,xmax’104’和xip_list’100,102’。

以下显示了两个具体示例:
图5.8 事物快照描述实例
在这里插入图片描述

第一个例子是’100:100:’。此快照表示以下内容(图5.8(a)):
由于xmin为100,因此等于或小于99的txids不活动。
等于或大于100的txids是活动的,因为xmax是100。

第二个例子是’100:104:100,102’。此快照表示以下内容(图5.8(b)):
等于或小于99的txids是不活动的。
等于或大于104的txids处于活动状态。
txids 100和102是活动的,因为它们存在于xip列表中,而txids 101和103不活动。

事务管理器提供事务快照。在READ COMMITTED隔离级别,只要执行SQL命令,事务就会获得快照;否则(REPEATABLE READ或SERIALIZABLE),事务只在执行第一个SQL命令时获取快照。获取的事务快照用于元组的可见性检查,如第5.7节中所述。

使用获取的快照进行可见性检查时,在实际中即使已提交或异常的快照中的活动事务,也必须将其视为in progress处理。此规则很重要,因为它会导致在READ COMMITTED和REPEATABLE READ(或SERIALIZABLE)之间的行为不同。我们在以下部分中反复参考此规则。

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

图5.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)是in progress。因此,从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_B不再是Transaction_B中不可见的。

Transaction_C不需要事务快照,因为它处于REPEATABLE READ级别并使用获得老的快照,即’200:200:’。因此,对于Transaction_C,Transaction_A仍然是不可见的。

5.6 可见性检查规则

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

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

5.6.1 t_xmin的状态为ABORTED

t_xmin状态为ABORTED的元组始终不可见(规则1),因为插入此元组的事务是异常的终止的
在这里插入图片描述
该规则明确表示为以下数学表达式。
Rule 1: If Status(t_xmin) = ABORTED ⇒ Invisible

5.6.2 t_xmin的状态为IN_PROGRESS

t_xmin状态为IN_PROGRESS的元组基本上是不可见的(规则3和4),但在一个条件下除外。
在这里插入图片描述

如果这个元组被另一个事务插入并且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),但在三个条件下除外。
在这里插入图片描述
规则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 可见性检查

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

5.7.1 可见性检查

图5.10显示了描述可见性检查的情况

图5.10 描述可见性检查
在这里插入图片描述
如图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命令如何为每个元组执行可见性检查。

T3的SELECT命令:
在T3,表tbl中只有一个Tuple_1,它在规则6中可见;因此,两个事务中的SELECT命令都返回’Jekyll’。

  • Rule6(Tuple_1) ⇒ Status(t_xmin:199) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible
    在这里插入图片描述

T5的SELECT命令:
首先,我们探索由txid 200执行的SELECT命令.Tuple_1是规则7不可见的,而Tuple_2是规则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
    在这里插入图片描述

另一方面,在由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
    在这里插入图片描述

如果更新的元组在提交之前从其他事务中可见,则它们被称为Dirty Reads,也称为wr-conflicts。但是,如上所示,PostgreSQL中的任何隔离级别都不会出现脏读。

T7的SELECT命令:

在下文中,描述了T7的SELECT命令在两个隔离级别中的行为。

首先,我们探讨txid 201何时处于READ COMMITTED级别。在这种情况下,txid 200被视为COMMITTED,因为事务快照是’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

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

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

  • 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

在这里插入图片描述

hint bits
为了获得事务的状态,PostgreSQL内部提供了三个函数,即TransactionIdIsInProgress,TransactionIdDidCommit和TransactionIdDidAbort。这些功能是为了减少对clog的频繁访问,例如缓存。但是,如果在检查每个元组时执行它们,则会发生瓶颈。
为了解决这个问题,PostgreSQL使用了提示位,如下所示。
在这里插入图片描述
在读取或写入元组时,如果可能的话,PostgreSQL会将提示位设置为元组的t_informask。例如,假设PostgreSQL检查元组的t_xmin的状态并获得状态为COMMITTED。在这种情况下,PostgreSQL将一个提示位HEAP_XMIN_COMMITTED设置为元组的t_infomask。如果已设置提示位,则不再需要TransactionIdDidCommit和TransactionIdDidAbort。因此,PostgreSQL可以有效地检查每个元组的t_xmin和t_xmax的状态。

5.7.2 Postgresql中REPEATABLE READ隔离级别的幻读

ANSI SQL-92标准中定义的REPEATABLE READ允许Phantom Reads。但是,PostgreSQL不允许幻读。实际上,是SI不允许幻影读取。

假设两个事务(即Tx_A和Tx_B)同时运行。它们的隔离级别为READ COMMITTED和REPEATABLE READ,它们的txids分别为100和101。首先,Tx_A插入一个元组。然后,它提交了事物。插入的元组的t_xmin为100.接着,Tx_B执行SELECT命令;但是,Tx_A插入的元组对于规则5是不可见的。因此,不会发生幻读。

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

在这里插入图片描述

5.8 防止丢失更新

丢失更新(也称为ww-conflict)是并发事务更新相同行时发生的异常,必须在REPEATABLE READ和SERIALIZABLE级别中防止它。本节介绍PostgreSQL如何防止丢失更新并显示示例。

5.8.1 当前update命令的行为

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

伪代码
在这里插入图片描述
(1)获取UPDATE命令将要更新的每一行。
(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冲突。

此函数为每个目标行执行更新操作。它有一个while循环来更新每一行,while循环的内部根据图5.11所示有三个条件分支块。

图5.11 ExecUpdate中的三个内部块
在这里插入图片描述
[1]目标行正在更新(图5.11 [1])
“正在更新”意味着该行由另一个并发事务正在更新,并且其事务尚未终止。在这种情况下,当前事务必须等待更新目标行的事务的终止,因为PostgreSQL的SI使用first-updater-win方案。例如,假设事务Tx_A和Tx_B同时运行,并且Tx_B尝试更新行;但是,Tx_A已更新它并且仍在进行中。在这种情况下,Tx_B等待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 例子

以下三个示例。第一个和第二个示例显示目标行正在被更新时的行为,第三个示例显示目标行已经被更新的行为。

例1
事务Tx_A和Tx_B更新同一个表中的同一行,它们的隔离级别为READ COMMITTED。
在这里插入图片描述
Tx_B执行如下。

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

例2
Tx_A和Tx_B更新同一表中的同一行,它们的隔离级别分别为READ COMMITTED和REPEATABLE READ。
在这里插入图片描述
Tx_B的行为描述如下。

1)执行UPDATE命令后,Tx_B应等待Tx_A的终止(ExecUpdate中的步骤(4))。
2)提交Tx_A后,中止Tx_B以解决冲突,因为目标行已更新且此事务的隔离级别为REPEATABLE READ(ExecUpdate中的步骤(5)和(6))。

例3
Tx_B(REPEATABLE READ)尝试更新已提交的Tx_A已更新的目标行。在这种情况下,中止Tx_B(ExecUpdate中的步骤(2),(8),(9)和(11))。
在这里插入图片描述

5.9 序列化快照隔离级别

从版本9.1开始,序列化快照隔离(SSI)已嵌入到SI中,以实现真正的SERIALIZABLE隔离级别。由于SSI的解释不那么简单,因此仅解释概要。有关详细信息,请参阅[2]

在下文中,使用下面所示的技术术语而没有定义。如果您不熟悉这些术语,请参阅[1,3]。

  • 优先级图(也称为依赖图和序列化图)
  • 序列化异常(例如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。在这种情况下,存在两个rw冲突,它们在该时间表的优先级图中形成一个循环,如图5.12(2)所示。因此,该调度具有序列化异常,即Write-Skew。

图5.12 Write-Skew调度及其优先级图
在这里插入图片描述
从概念上讲,存在三种类型的冲突:wr-conflict(Dirty Reads),ww-conflict(Lost Updates)和rw-conflict。但是,不需要考虑wr-和ww-confict,因为如前面部分所示,PostgreSQL可以防止此类冲突。因此,PostgreSQL中的SSI实现只需要考虑rw-conflict。

PostgreSQL为了SSI的实现,采用以下策略:

  1. 利用SIREAD LOCK(谓词锁)记录每一个事务访问的对象(tuple、page和relation)。
  2. 无论何时写入任何堆或索引元组,都使用SIREAD锁来检测rw-conflict。
  3. 如果检测到了rw-conflict(序列化异常),则中止事务。
5.9.2 Postgresql 中的SSI

注意
为简单起见,本文档中省略了一些重要的数据结构,例如SERIALIZABLEXACT。因此,函数的解释,即CheckTargetForConflictOut,CheckTargetForConflictIn和PreCommit_CheckForSerializationFailure,也极为简化。例如,我们指出哪些功能检测到了冲突;但是,没有详细解释如何检测到冲突。如果您想了解详细信息,请参阅源代码:predicate.c。

SIREAD locks:
内部称为谓词锁的SIREAD锁包含锁定的对象(tuple、page和relation)和(虚拟)txids,txid用于存储谁访问了哪个对象的信息。请注意,省略了对虚拟txid的描述。这里使用txid来简化说明虚拟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-scan时,只读取索引页而不读取表页。

SIREAD锁有三个级别:Tuple, page, 和relation。如果一个页面中所有行都创建了SIREAD锁,则会为这个页将它们聚合到一个页面级别SIREAD锁中,并释放(删除)相关tuple级别SIREAD锁,以减少内存空间。对于所有页面的读取也是如此。

为索引创建SIREAD锁时,将开始创建页级别SIREAD锁。使用顺序扫描时,无论是否存在索引和/或WHERE子句,都会创建relation级别SIREAD锁。请注意,在特定情况下,这个操作可能会导致序列化异常的false-positive检测。详细信息在第5.9.4节中描述。

rw-conflicts:
rw-conflict就是有一个SIREAD LOCK,还有分别read和write这个SIREAD LOCK中的对象的两个并发的Serializable事务。

只要在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冲突{r = 100,w = 101,{Tuple_1}}。

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

5.9.3 SSI如何执行

在这里,我们描述了SSI如何解决Write-Skew异常。我们使用如下所示的表tbl:
在这里插入图片描述
事务Tx_A和Tx_B执行以下命令(图5.13)。

图5.13 write-skew
在这里插入图片描述
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.14。

图5.14 图5.13所示场景中索引与表之间的关系
在这里插入图片描述

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

图5.15 SIREAD锁和rw冲突,以及图5.13所示场景的时间表
在这里插入图片描述
T1:
当Tx_A执行SELECT命令时,CheckTargetForConflictsOut会创建SIREAD锁。在这种情况下,该函数创建两个SIREAD锁:L1和L2。

L1和L2分别与Pkey_2和Tuple_2000相关联。

T2:
执行Tx_B的SELECT命令时,CheckTargetForConflictsOut会创建两个SIREAD锁:L3和L4。

L3和L4分别与Pkey_1和Tuple_1相关联。

T3:
执行Tx_A的UPDATE命令时,在ExecUpdate之前和之后都会调用CheckTargetForConflictsOut和CheckTargetForConflictsIN。
在这种情况下,CheckTargetForConflictsOut什么都不做。
CheckTargetForConflictsIn创建rw-conflict C1,这是Pkey_1和Tuple_1在Tx_B和Tx_A之间的冲突,因为Pkey_1和Tuple_1都由Tx_B读取并由Tx_A写入。

T4:
当执行Tx_B的UPDATE命令时,CheckTargetForConflictsIn会创建rw-conflict C2,这是Tx_A和Tx_B之间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_A已提交,因为Tx_B仍在进行中。

T6:
当Tx_B尝试提交时,PreCommit_CheckForSerializationFailure检测到序列化异常并且Tx_A已经提交;因此,Tx_B被中止。

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

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

图5.16 其他write-skew情况
在这里插入图片描述

Wiki解释了几个更复杂的异常现象。

5.9.4 False-Positive序列化异常

在SERIALIZABLE模式下,因为False-Positive异常不会被检测到,所以始终完全保证并发事务的可串行化。但是,在某些情况下,可以检测到false-positive;因此,用户在使用SERIALIZABLE模式时应牢记这一点。在下文中,描述了PostgreSQL检测到false-positive的情况。

图5.17显示了发生False-Positive序列化异常的情况

图5.17
在这里插入图片描述
当使用顺序扫描时,如SIREAD锁的解释中所述,PostgreSQL创建了一个关系级别SIREAD锁。图5.18(1)显示了PostgreSQL使用顺序扫描时的SIREAD锁和rw-confict。在这种情况下,与tbl的SIREAD锁相关联的rw-conflicts C1和C2被创建,并且它们在优先级图中创建一个循环。因此,检测到false-positive Write-Skew异常(即使没有冲突,Tx_A或Tx_B也将被中止)。

图5.18 False-positive异常(1) – 使用顺序扫描
在这里插入图片描述
即使使用索引扫描,如果事务Tx_A和Tx_B都获得相同的索引SIREAD锁,PostgreSQL也会检测到false-positive异常。图5.19显示了这种情况。假设索引页Pkey_1包含两个索引项,其中一个指向Tuple_1,另一个指向Tuple_2。当Tx_A和Tx_B执行相应的SELECT和UPDATE命令时,Pkey_1 由Tx_A和Tx_B读取和写入。在这种情况下,两个与Pkey_1相关联的rw-conflict的C1和C2在优先级图中创建一个循环;因此,检测到false-positive Write-Skew异常。 (如果Tx_A和Tx_B获取不同索引页的SIREAD锁,则不会检测到false-positive,并且可以提交两个事务。)

5.19 False-positive异常(2) – 使用相同索引页的索引扫描
在这里插入图片描述

5.10 必要的维护过程

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

1.删除dead tuple和指向dead tuple的索引tuple
2.去除没有必要的clog
3. 冻结旧的txids
4. 更新FSM,VM和统计信息

第5.3.2和5.4.3节分别解释了对第一和第二过程的需求。第三个过程与事务id循环有关,在下面的小节中对此进行了简要描述。

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

5.10.1 FREEZE 处理

在这里,我们描述了txid的Wraparound problem。

假设插入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中所谓的Wraparound problem。

图5.20 Wraparound problem
在这里插入图片描述
为了解决这个问题,PostgreSQL引入了一个名为frozen txid的概念,并执行一个FREEZE的过程。

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

vacuum处理过程会调用freeze进程。如果t_xmin值比当前txid减去vacuum_freeze_min_age(默认值为5000万)的值更老,则冻结过程将扫描所有表文件并将tuples的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)。

图5.21 freeze处理
在这里插入图片描述

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值