PostgreSQL 并发控制

2020-06-17 00:00:00 更新 命令 事务 状态 快照

第5章


并发控制是一种机制,当数个事务在数据库中同时运行时,它保持ACID的两个属性即一致性和隔离性。
共有三种广泛的并发控制技术,即 多版本并发控制(MVCC), 严格两阶段锁定(S2PL)和乐观并发控制(OCC),并且每种技术都有许多不同之处。在MVCC中,每个写操作都会创建数据项的新版本,同时保留旧版本。当事务读取数据项时,系统选择一种版本以确保隔离单个事务。MVCC的主要优点是' 读者不会阻止作家,而作家也不会阻止读者相比之下,例如,基于S2PL的系统必须在写入者写入项目时阻止读取器,因为写入者获得了该项目的排他锁。PostgreSQL和某些RDBMS使用MVCC的一种变体,称为快照隔离(SI)
为了实现SI,某些RDBMS(例如Oracle)使用回滚段。写入新数据项时,该项目的旧版本将写入回滚段,随后新项目将被覆盖到数据区域。PostgreSQL使用更简单的方法。新的数据项直接插入到相关的表格页面中。读取项目时,PostgreSQL通过应用可见性检查规则来响应单个事务选择项目的适当版本。
SI不允许在ANSI SQL-92标准中定义三个异常,即“ 脏读”,“ 不可重复读 ”和“ 幻像读”。但是,SI无法实现真正​​的可序列化性,因为它允许序列化异常,例如Write SkewRead-only Transaction Skew。请注意,基于经典可序列化性定义的ANSI SQL-92标准等同于现代理论中的定义。为了解决此问题,可序列化快照隔离(SSI)从9.1版开始已添加。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章中进行了介绍。

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

PostgreSQL中的事务隔离级别
下表描述了PostgreSQL实现的事务隔离级别:
隔离度脏读不可重复读幻影阅读序列化异常读已提交不可能可能可能可能可重复读取* 1不可能不可能在PG中不可能;参见第5.7.2节。
(在ANSI SQL中可能)可能可序列化不可能不可能不可能不可能* 1:在9.0及更早版本中,此级别被用作“ SERIALIZABLE”,因为它不允许ANSI SQL-92标准中定义的三个异常。但是,随着9.1版中SSI的实现,该级别已更改为“ REPEATABLE READ”,并引入了真正的SERIALIZABLE级别。



PostgreSQL将SSI用于DML(数据操作语言,例如SELECT,UPDATE,INSERT,DELETE),将2PL用于DDL(数据定义语言,例如CREATE TABLE等)。


5.1。交易编号
每当事务开始时,事务管理器都会分配一个的标识符,称为事务ID(txid)。PostgreSQL的txid是一个32位无符号整数,大约为42亿(千百万)。如果在事务开始后执行内置的txid_current()函数,该函数将按如下所示返回当前的txid。
TESTDB =#BEGIN ; 开始 testdb =#SELECT txid_current (); txid_current -------------- 100 (1 行)
PostgreSQL保留以下三个特殊的txid:

  • 表示 txid。
  • 1表示Bootstrap txid,仅用于数据库集群的初始化。
  • 2表示冻结的 txid,如第5.10.1节所述。

Txid可以相互比较。例如,从txid 100的角度来看,大于100的txid是“将来的”,并且它们在txid 100中是不可见的。小于100的txid是“过去的”并且可见(图5.1 a))。
图5.1。PostgreSQL中的交易ID。


由于在实际系统中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)。
图5.2。元组结构。




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

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

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


5.3。插入,删除和更新元组
本节介绍如何插入,删除和更新元组。然后,简要介绍用于插入和更新元组的自由空间图(FSM)
为了专注于元组,下面不显示页眉和行指针。图5.3显示了一个如何表示元组的示例。
图5.3。元组的表示形式。


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


假定通过txid为99的事务将元组插入页面中。在这种情况下,插入元组的标头字段设置如下。

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



页面检查
PostgreSQL提供了一个扩展pageinspect,它是一个贡献模块,用于显示数据库页面的内容。
testdb =#CREATE EXTENSION pageinspect ; 创建扩展 testdb =#CREATE TABLE tbl (数据文本); 创建表 testdb =#插入tbl值('A' ); INSERT 0 1 testdb =#选择lp 作为元组,t_xmin ,t_xmax ,t_field3 作为t_cid ,t_ctid FROM heap_page_items (get_raw_page ('tbl' ,0 )); 元组| t_xmin | t_xmax | t_cid | t_ctid ------- + -------- + -------- + ------- + -------- 1 | 99 | 0 | 0 | (0 ,1 )(1 行)


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


假定Tuple_1被txid 111删除。在这种情况下,Tuple_1的标头字段设置如下。

  • 元组_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的标头字段如下。

  • 元组_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将成为死元组。
5.3.4。自由空间地图
插入堆或索引元组时,PostgreSQL使用相应表或索引的FSM选择可以插入的页面。
如1.2.3节所述,所有表和索引都有各自的FSM。每个FSM将有关每个页面的可用空间容量的信息存储在相应的表或索引文件中。
所有FSM都带有后缀'fsm',并且在必要时将它们加载到共享内存中。

pg_freespacemap
扩展pg_freespacemap提供指定表/索引的可用空间。以下查询显示指定表中每个页面的可用空间比率。
testdb =#创建扩展pg_freespacemap ; 创建扩展 TESTDB =#SELECT *,轮(100 * 无济于事/ 8192 ,2 )作为“自由空间比” FROM pg_freespace ('帐户' ); blkno | 果| 自由空间比率 ------- + ------- + ----------------- 0 | 7904 | 96.00 1 | 7520 | 91.00 2 | 7136 | 87.00 3 | 7136 | 87.00 4 | 7136 | 87.00 5 | 7136 | 87.00 ....



5.4。提交日志(日志)
PostgreSQL的持有交易的状态提交日志。提交日志,通常被称为堵塞,被分配到共享存储器,并在整个事务处理被使用。
本节将介绍在PostgreSQL中,交易的状态如何堵塞操作,并且堵塞的维护。
5.4.1交易状态
PostgreSQL的定义了四种交易状态,即IN_PROGRESS,提交中止,SUB_COMMITTED。
前三个状态是显而易见的。例如,当一个交易过程中,它的地位是IN_PROGRESS等。
SUB_COMMITTED是次交易,而这个文件中省略其描述。
5.4.2。木log的表现
障碍物在共享内存中包含一个或多个8 KB页面。障碍物逻辑上形成一个数组。数组的索引对应于相应的交易ID,并且数组中的每个项目都保持相应交易ID的状态。图5.7显示了木log及其操作方式。
图5.7。the的操作方式。



  • T1: txid 200提交;txid 200的状态从IN_PROGRESS更改为COMMITTED。
  • T2: txid 201中止;txid 201的状态从IN_PROGRESS更改为ABORTED。


当前的txid前进时,并且障碍物无法再存储它时,将附加一个新页面。
当需要交易状态时,将调用内部功能。这些函数读取阻塞并返回所请求事务的状态。(参见“提示位”中第5.7.1节)。
5.4.3。维护木log
当PostgreSQL关闭或运行检查点进程时,将阻塞的数据写入存储在pg_clog子目录下的文件中。(需要注意的是pg_clog里将改名为pg_xact在第10版)这些文件被命名为00000001,等的大文件大小为256 KB。例如,当木log使用八页(页到第八页;总大小为64 KB)时,其数据被写入0000(64 KB),而在37页(296 KB)中,数据被写入分为0000和0001,大小分别为256 KB和40 KB。
PostgreSQL启动时,将加载pg_clog的文件(pg_xact的文件)中存储的数据以初始化该clog。
堵塞物的大小持续增加,因为每当堵塞物被填满时都会添加新页面。但是,并非阻塞中的所有数据都是必需的。第6章中介绍的真空处理会定期删除这些旧数据(阻塞页面和文件)。有关删除阻塞数据的详细信息,请参见第6.4节。
5.5。交易快照
交易快照是存储的信息有关的所有交易是否活跃,在一定的时间点为个人交易的数据集。在这里,活动事务意味着它正在进行中或尚未开始。
PostgreSQL内部将事务快照的文本表示格式定义为'100:100:'。例如,“ 100:100:”表示“小于99的txid未激活,而等于或大于100的txid已激活”。在以下描述中,使用了这种方便的表示形式。如果您不熟悉,请参阅 下面。

内置函数txid_current_snapshot及其文本表示格式
函数txid_current_snapshot显示当前事务的快照。
testdb =#SELECT txid_current_snapshot (); txid_current_snapshot ----------------------- 100 :104 :100 ,102 (1 行)
txid_current_snapshot的文本表示为'xmin:xmax:xip_list',其组成部分描述如下。

早的txid仍处于活动状态。所有先前的事务将被提交并可见,或者被回滚并终止。

  • 大值

个尚未分配的txid。截至快照时,所有大于或等于此值的txid尚未启动,因此不可见。

  • xip_list

快照时的活动txid。该列表仅包括xmin和xmax之间的活动txid。例如,在快照“ 100:104:100,102”中,xmin为“ 100”,xmax为“ 104”,xip_list为“ 100,102”。
下面显示了两个特定的示例:
图5.8。事务快照表示的示例。


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

  • txids是等于或小于99是 活跃的,因为XMIN是100。
  • 等于或大于100的txid 处于活动状态,因为xmax为100。

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

  • txids是等于或小于99是 活跃的
  • 等于或大于104的txid 有效
  • 由于txid 100和102 存在于xip列表中,因此它们是活动的,而txid 101和103 处于活动状态



事务快照由事务管理器提供。在READ COMMITTED隔离级别中,无论何时执行SQL命令,事务都会获取快照。否则(REPEATABLE READ或SERIALIZABLE),该事务仅在执行个SQL命令时获得快照。所获得的事务快照用于元组的可见性检查,这在5.7节中进行了描述。
使用获取的快照进行可见性检查时, 即使快照中的活动事务实际上已被提交或中止,也必须将它们视为正在进行中。该规则很重要,因为它导致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)正在进行中。因此,从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。可见性检查规则
可见性检查规则是一组规则,用于使用元组的t_xmin和t_xmax,阻塞和获取的事务快照来确定每个元组是可见还是不可见。这些规则太复杂,无法详细解释。因此,本文档显示了后续描述所需的小规则。在下文中,我们省略了与子交易相关的规则,并忽略了有关t_ctid的讨论,即,我们不考虑在事务中已被两次更新两次以上的元组。
所选规则的数量为10,可以分为三种情况。
5.6.1。t_xmin的状态为ABORTED
t_xmin状态为ABORTED的元组始终不可见(规则1),因为插入该元组的事务已中止。
/ * t_xmin状态= ABORTED * / 规则1 :IF t_xmin状态是'ABORTED' THEN RETURN '隐身' END IF
该规则明确表示为以下数学表达式。

  • 规则1:如果Status(t_xmin)=已终止⇒不可见


5.6.2。t_xmin的状态为IN_PROGRESS
t_xmin状态为IN_PROGRESS的元组本质上是不可见的(规则3和4),除非在一种情况下。
/ * t_xmin状态= IN_PROGRESS * / IF t_xmin状态是'IN_PROGRESS' THEN IF t_xmin = current_txid THEN 规则2 :IF t_xmax = INVALID THEN RETURN '可见' 规则3 :ELSE / *这个元组已经由当前事务删除或更新本身。* / RETURN '隐形' END IF 规则4 :ELSE / * t_xmin≠current_txid * / RETURN '隐形' END IF END IF
如果该元组被另一个事务插入,并且t_xmin的状态为IN_PROGRESS,则该元组显然是不可见的(规则4)。
如果t_xmin等于当前txid(即,该元组由当前事务插入),并且t_xmax 不是 INVALID,则该元组是不可见的, 因为它已被当前事务更新或删除(规则3)。
例外情况是这样的情况,其中该元组被当前事务插入并且t_xmax为INVALID。在这种情况下,该元组在当前事务中可见(规则2)。

  • 规则2:如果Status(t_xmin)= IN_PROGRESS∧t_xmin = current_txid∧t_xmax = INVAILD⇒可见
  • 规则3:如果Status(t_xmin)= IN_PROGRESS∧t_xmin = current_txid∧t_xmax≠INVAILD⇒不可见
  • 规则4:如果Status(t_xmin)= IN_PROGRESS∧t_xmin≠current_txid⇒不可见


5.6.3。t_xmin的状态为COMMITTED
t_xmin状态为COMMITTED的元组 可见(规则6,8和9),但在三种情况下除外。
/ * t_xmin状态= COMMITTED * / IF t_xmin状态是'COMMITTED' THEN 规则5 :IF t_xmin 是活性在所获得的交易快照THEN RETURN '隐身' 规则6 :ELSE IF t_xmax = 或t_xmax的状态是'ABORTED' THEN RETURN '可见' ELSE IF t_xmax状态是'IN_PROGRESS' THEN 规则7 :IF t_xmax = current_txid THEN RETURN '隐身' 规则8 :ELSE / * t_xmax≠current_txid * / RETURN '可见' END IF ELSE IF t_xmax状态是'COMMITTED' THEN 规则9 :IF t_xmax 是活性在所获得的交易快照THEN RETURN '可见' 规则10 :否则返回 “不可见” 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视为进行中。

  • 规则5:如果状态(t_xmin)=提交∧快照(t_xmin)=激活⇒不可见
  • 规则6:如果Status(t_xmin)= COMMITTED∧(t_xmax = INVALID∨Status(t_xmax)= ABORTED)⇒可见
  • 规则7:如果Status(t_xmin)= COMMITTED∧Status(t_xmax)= IN_PROGRESS∧t_xmax = current_txid⇒不可见
  • 规则8:如果Status(t_xmin)= COMMITTED∧Status(t_xmax)= IN_PROGRESS∧t_xmax≠current_txid⇒可见
  • 规则9:如果状态(t_xmin)=提交∧状态(t_xmax)=提交∧快照(t_xmax)=激活⇒可见
  • 规则10:如果Status(t_xmin)= COMMITTED∧Status(t_xmax)= COMMITTED∧快照(t_xmax)≠活动⇒不可见


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。txid200的隔离级别为READ COMMITTED,txid 201的隔离级别为READ COMMITTED或REPEATABLE READ。
我们探索SELECT命令如何对每个元组执行可见性检查。
T3的SELECT命令:
在T3处,表tbl中只有一个Tuple_1,并且可由规则6 看到;因此,两个事务中的SELECT命令都返回“ Jekyll”。

  • Rule6(元组_1)⇒状态(t_xmin:199)=提交∧t_xmax =⇒可见

testdb =#- txid 200 testdb =#SELECT * FROM tbl ; 名称 -------- Jekyll (1 行)
testdb =#- txid 201 testdb =#SELECT * FROM tbl ; 名称 -------- Jekyll (1 行)
T5的SELECT命令:
首先,我们探索由txid 200执行的SELECT命令。Tuple_1在规则7中是不可见的,Tuple_2在规则2中是可见的;因此,此SELECT命令返回“ Hyde”。

  • Rule7(元组_1):状态(t_xmin:199)=提交∧状态(t_xmax:200)= IN_PROGRESS∧t_xmax:200 = current_txid:200⇒不可见
  • Rule2(元组_2):状态(t_xmin:200)= IN_PROGRESS∧t_xmin:200 = current_txid:200∧t_xmax =⇒可见

testdb =#- txid 200 testdb =#SELECT * FROM tbl ; 名称 ------ 海德(1 行)

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

  • Rule8(元组_1):状态(t_xmin:199)=提交∧状态(t_xmax:200)= IN_PROGRESS∧t_xmax:200≠current_txid:201⇒可见
  • Rule4(元组_2):状态(t_xmin:200)= IN_PROGRESS∧t_xmin:200≠current_txid:201⇒不可见


testdb =#- txid 201 testdb =#SELECT * FROM tbl ; 名称 -------- Jekyll (1 行)
如果在提交之前从其他事务中可以看到更新的元组,则将它们称为脏读,也称为wr-conflicts。但是,如上所述,在PostgreSQL中任何隔离级别都不会发生脏读。
T7的SELECT命令:
下面,描述两个隔离级别中T7的SELECT命令的行为。
首先,我们探讨txid 201何时处于READ COMMITTED级别。在这种情况下,txid 200被视为COMMITTED,因为事务快照为'201:201:'。因此,Tuple_1是看不见的第10和Tuple_2是可见第6,和SELECT命令返回“海德”。

  • Rule10(元组_1):状态(t_xmin:199)=提交∧状态(t_xmax:200)=提交∧快照(t_xmax:200)≠活动⇒不可见
  • Rule6(元组_2):状态(t_xmin:200)=提交∧t_xmax =⇒可见


testdb =#- txid 201 (读取已提交) testdb =#SELECT * FROM tbl ; 名称 ------ 海德(1 行)
请注意,在提交txid 200之前和之后执行的SELECT命令的结果不同。这通常被称为不可重复读
相反,当txid 201处于REPEATABLE READ级别时,由于事务快照为'200:200:',因此必须将txid 200视为IN_PROGRESS。因此,Tuple_1是可见规则9和Tuple_2是看不见的规则5,和SELECT命令返回“化身”。请注意,不可重复读取不会在“可重复读取”(和“可序列化”)级别中发生。

  • Rule9(元组_1):状态(t_xmin:199)=提交∧状态(t_xmax:200)=提交∧快照(t_xmax:200)=活动⇒可见
  • Rule5(元组_2):状态(t_xmin:200)=提交∧快照(t_xmin:200)=激活⇒不可见


testdb =#- txid 201 (可重复读取) testdb =#SELECT * FROM tbl ; 名称 -------- Jekyll (1 行)

提示位
为了获得事务的状态,PostgreSQL内部提供了三个函数,即TransactionIdIsInProgress,TransactionIdDidCommit和TransactionIdDidAbort。实施这些功能是为了减少对堵塞的频繁访问,例如缓存。但是,只要在检查每个元组时执行瓶颈,就会发生瓶颈。
为了解决这个问题,PostgreSQL使用提示位,如下所示。
#define HEAP_XMIN_COMMITTED 0x0100 / * t_xmin已提交* / #define HEAP_XMIN_INVALID 0x0200 / * t_xmin/已终止* / #define HEAP_XMAX_COMMITTED 0x0400 / * t_xmax已提交* / #define HEAP_XMAX_INVALID 0x0800 / *

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


5.7.2。幻影读取在PostgreSQL的可重复读取级别中
ANSI SQL-92标准中定义的REPEATABLE READ允许Phantom Reads。但是,PostgreSQL的实现不允许它们。原则上,SI不允许幻像读取。
假设两个事务,即Tx_A和Tx_B,正在同时运行。它们的隔离级别为READ COMMITTED和REPEATABLE READ,其txid分别为100和101。首先,Tx_A插入一个元组。然后,将其提交。插入的元组的t_xmin为100。接下来,Tx_B执行SELECT命令;否则,执行SELECT命令。然而,通过插入Tx_A元组是不可见的规则5。因此,不会发生幻像读取。

  • Rule5(新元组):状态(t_xmin:100)=提交∧快照(t_xmin:100)=激活⇒不可见


testdb =#- Tx_A :txid 100 testdb =#开始交易 testdb- # 隔离级别已提交; 开始交易 testdb =#INSERT tbl (id ,data ) 值(1 ,'phantom' ); 插入1 testdb =#COMMIT ; 承诺
testdb =#- Tx_B :txid 101 testdb =#开始交易 testdb- # 隔离级别可重复读取; 开始交易 testdb =#SELECT txid_current (); txid_current -------------- 101 (1 行) testdb =#SELECT * FROM tbl,其中id = 1 ; id | 数据 ---- + ------ (0 行)

5.8。防止丢失的更新
一个失落的更新,也被称为WW冲突,是发生在并发事务更新同一行的异常,它必须同时可重复读和可系列化水平来预防。本节介绍PostgreSQL如何防止丢失更新并显示示例。
5.8.1。并发UPDATE命令的行为
执行UPDATE命令时,将在内部调用ExecUpdate函数。ExecUpdate的伪代码如下所示:

伪代码:ExecUpdate
(1 )用于将要更新的每一行由此UPDATE命令 (2 )WHILE真 / *块* / (3 )IF中的目标行中被更新THEN(4 )WAIT用于该更新的目标行交易的终止 (5 )IF(终止事务的状态是COMMITTED ) AND (的隔离级别这个交易是重复读或SERIALIZABLE )THEN(6 )ABORT此交易 / *更新器-赢* / ELSE(7 )GOTO步骤(2 )如果结束 / *第二个块* / (8 )ELSE如果目标行已被另一个并发事务更新,则THEN(9 )IF(此事务的隔离级别为READ COMMITTED THEN(10 )更新目标行 ELSE(11 )中止这项交易 / * First-Updater-Win * / END IF / *第三块* / ELSE / *目标行尚未修改或已被终止的事务更新。* / (12 )更新目标行 END IF END WHILE END FOR

  • (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)如果该事务的隔离级别是“读已提交”,则进行步骤(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的终止。

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

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

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

  • [3]没有冲突(图5.11 [3])

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


个更新者获胜/个提交者获胜
PostgreSQL基于SI的并发控制使用first-updater-win方案。相反,如下一节所述,PostgreSQL的SSI使用先提交者赢方案。


5.8.2。例子
下面显示了三个示例。个和第二个示例显示了目标行被更新时的行为,第三个示例显示了目标行被更新时的行为。
范例1:
事务Tx_A和Tx_B更新同一表中的同一行,其隔离级别为READ COMMITTED。
testdb =#- Tx_A testdb =#开始交易 testdb- # 隔离级别已提交; 开始交易 testdb =#更新tbl SET名称= '海德' ; 更新1 testdb =#COMMIT ; 承诺
testdb =#- Tx_B testdb =#开始交易 testdb- # 隔离级别已提交; 开始交易 testdb =#更新tbl SET名称= 'Utterson' ; ↓ ↓ 这个事务被被阻止 ↓ UPDATE 1

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。
testdb =#- Tx_A testdb =#开始交易 testdb- # 隔离级别已提交; 开始交易 testdb =#更新tbl SET名称= '海德' ; 更新1 testdb =#COMMIT ; 承诺
testdb =#- Tx_B testdb =#开始交易 testdb- # 隔离级别可重复读取; 开始交易 testdb =#更新tbl SET名称= 'Utterson' ; ↓ ↓ 这个事务被被阻止 ↓ ERROR :couldn “吨序列由于并发更新访问

Tx_B的行为描述如下。

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


范例3:
Tx_B(可重复读取)尝试更新已由提交的Tx_A更新的目标行。在这种情况下,Tx_B被中止(ExecUpdate中的步骤(2),(8),(9)和(11))。

testdb =#- Tx_A testdb =#开始交易 testdb- # 隔离级别已提交; 开始交易 testdb =#更新tbl SET名称= '海德' ; 更新1 testdb =#COMMIT ; 承诺
testdb =#- Tx_B testdb =#开始交易 testdb- # 隔离级别可重复读取; 开始交易 testdb =#SELECT * FROM tbl ; 名称 -------- Jekyll (1 行) testdb =#UPDATE tbl SET name = 'Utterson' ; 错误:由于并发更新,无法序列化访问

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冲突(脏读),ww冲突(丢失更新)和rw冲突。但是,不需要考虑wr和ww冲突,因为如前几节所示,PostgreSQL可以防止此类冲突。因此,PostgreSQL中的SSI实现只需要考虑rw-conflicts。
PostgreSQL对SSI实施采取以下策略:

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


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


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

  • SIREAD锁:

SIREAD锁(内部称为谓词锁)是一对对象和(虚拟)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锁,因为当应用第7.2节中描述的仅索引扫描功能时,仅读取索引页而不读取表页。

SIREAD锁具有三个级别:元组,页面和关系。如果在单个页面中创建了所有元组的SIREAD锁,则将它们聚集到该页面的单个SIREAD锁中,并释放(删除)关联元组的所有SIREAD锁,以减少内存空间。对于所有读取的页面来说都是一样的。

为索引创建SIREAD锁时,将从头开始创建页面级SIREAD锁。使用顺序扫描时,无论是否存在索引和/或WHERE子句,都会从头开始创建关系级别的SIREAD锁。请注意,在某些情况下,此实现可能导致序列化异常的假阳性检测。有关详细信息,请参见第5.9.4节。

  • rw-冲突:

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

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

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

当以SERIALIZABLE模式执行COMMIT命令时调用的CheckTargetForConflictOut和CheckTargetForConflictIn函数以及PreCommit_CheckForSerializationFailure函数均使用创建的rw-conflicts检查序列化异常。如果它们检测到异常,则仅提交个提交的事务,而其他事务则中止(通过第一个提交者胜出方案)。
5.9.3。SSI如何执行
在这里,我们描述SSI如何解决Write-Skew异常。我们使用如下所示的简单表tbl
testdb =#CREATE TABLE tbl (id INT主键,flag bool DEFAULT false ); TESTDB =#INSERT INTO TBL (ID )SELECT generate_series (1 ,2000 ); testdb =#ANALYZE tbl ;
事务Tx_A和Tx_B执行以下命令(图5.13)。
图5.13。写偏斜方案。


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

图5.14。图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异常。
图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-冲突C1,这是Tx_B和Tx_A之间的Pkey_1和Tuple_1的冲突,因为Pkey_1和Tuple_1都被Tx_B读取并由Tx_A写入。

  • T4:

当执行Tx_B的UPDATE命令时,CheckTargetForConflictsIn创建rw-冲突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之后(在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))。
图5.16。其他Write-Skew方案。




该Wiki解释了一些更复杂的异常。

5.9.4。假阳性序列化异常

在SERIALIZABLE模式下,始终可以完全保证并发事务的可序列化性,因为永远不会检测到错误的负序列化异常。但是,在某些情况下,可以检测到假阳性异常。因此,用户在使用SERIALIZABLE模式时应牢记这一点。在下文中,描述了PostgreSQL检测到假阳性异常的情况。
图5.17显示了发生假阳性序列化异常的情况。
图5.17。出现假阳性序列化异常的情况。


在使用顺序扫描时,如SIREAD锁的说明中所述,PostgreSQL创建一个关系级别的SIREAD锁。图5.18(1)显示了PostgreSQL使用顺序扫描时的SIREAD锁和rw-冲突。在这种情况下,将创建与tbl的SIREAD锁关联的rw冲突C1和C2,并且它们在优先级图中创建一个循环。因此,检测到假阳性的Write-Skew异常(即使没有冲突,Tx_A或Tx_B也将中止)。
图5.18。假阳性异常(1)–使用顺序扫描。


即使使用索引扫描,如果事务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-冲突C1和C2都与Pkey_1相关联,它们在优先级图中创建了一个循环。因此,检测到假阳性的Write-Skew异常。(如果Tx_A和Tx_B获得了不同索引页的SIREAD锁,则不会检测到假阳性,并且可以提交两个事务。)
图5.19。假阳性异常(2)–使用相同的索引页进行索引扫描。




5.10。必要的维护流程

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

  1. 删除死元组和索引元组,该元组指向相应的死元组
  2. 删除木unnecessary的不必要部分
  3. 冻结旧的txids
  4. 更新FSM,VM和统计信息

第5.3.2节和第5.4.3节分别解释了对和第二个过程的需求。第三个过程与事务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添加为2.1十亿+ 101。然而,Tuple_1是不再可见因为TxID添加100是在未来(图5.20)。这就是PostgreSQL中所谓的事务环绕问题
图5.20。环绕式问题。


为了解决这个问题,PostgreSQL引入了一个称为Frozen txid的概念,并实现了一个称为FREEZE的过程。
在PostgreSQL中,冻结的txid(是特殊的保留txid 2)定义为始终比所有其他txid更旧。换句话说,冻结的txid始终是不活动的并且可见。
冻结过程由真空过程调用。如果t_xmin值早于当前txid减去vacuum_freeze_min_age(默认值为5000万),则冻结过程将扫描所有表文件并将元组的t_xmin重写为冻结的txid 。这将在第6章中更详细地说明。
例如,从图5.21a)中可以看出,当前的txid为5000万,冻结过程由VACUUM命令调用。在这种情况下,Tuple_1和Tuple_2的t_xmin都重写为2。
在版本9.4或更高版本中,XMIN_FROZEN位设置为元组的t_infomask字段,而不是将元组的t_xmin重写为冻结的txid(图5.21 b)。
图5.21。冻结过程。


参考文献


  • [1] Abraham Silberschatz,Henry F. Korth和S. Sudarshan,“ 数据库系统概念 ”,麦格劳-希尔教育,ISBN-13:978-0073523323
  • [2] Dan RK Ports和Kevin Grittner ,“ PostgreSQL中的可序列化快照隔离 ”,VDBL 2012
  • [3] Thomas M. Connolly和Carolyn E. Begg,“ 数据库系统 ”,皮尔森,ISBN-13:978-0321523068

相关文章