合 《PostgreSQL技术内幕——原理探索》第五章 并发控制
Tags: PGPostgreSQL翻译《PostgreSQL技术内幕——原理探索》并发控制
- PostgreSQL中的事务隔离等级
- 5.1 事务标识
- 5.2 元组结构
- 5.3 元组的增删改
- 5.3.1 插入
- pageinspect
- 5.3.2 删除
- 5.3.3 更新
- 5.3.4 空闲空间映射
- pg_freespacemap
- 5.4 提交日志(clog)
- 5.4.1 事务状态
- 5.4.2 提交日志如何工作
- 5.4.3 提交日志的维护
- 5.5 事务快照
- 内置函数txid_current_snapshot及其文本表示
- 5.6 可见性检查规则
- 5.6.1 t_xmin的状态为ABORTED
- 5.6.2 t_xmin的状态为IN_PROGRESS
- 5.6.3 t_xmin的状态为COMMITTED
- 5.7 可见性检查
- 5.7.1 可见性检查
- 提示位(Hint Bits)
- 5.7.2 PostgreSQL可重复读等级中的幻读
- 5.8 防止丢失更新
- 5.8.1 并发UPDATE命令的行为
- 伪代码:ExecUpdate
- 以先更新者为准 / 以先提交者为准
- 5.8.2 例子
- 例1
- 例2
- 例3
- 5.9 可串行化快照隔离
- 5.9.1 SSI实现的基本策略
- 5.9.2 PostgreSQL的SSI实现
- SIREAD锁
- 读-写冲突
- 5.9.3 SSI的原理
- 5.9.4 假阳性的串行化异常
- 5.10 所需的维护进程
- 5.10.1 冻结处理
- 参考文献
当多个事务同时在数据库中运行时,并发控制是一种用于维持一致性与隔离性的技术,一致性与隔离性是ACID的两个属性。
从宽泛的意义上来讲,有三种并发控制技术:多版本并发控制(Multi-version Concurrency Control, MVCC),严格两阶段锁定(Strict Two-Phase Locking, S2PL)和乐观并发控制(Optimistic Concurrency Control, OCC),每种技术都有多种变体。在MVCC中,每个写操作都会创建一个新版本的数据项,并保留其旧版本。当事务读取数据对象时,系统会选择其中的一个版本,通过这种方式来确保各个事务间相互隔离。 MVCC的主要优势在于“读不会阻塞写,而写也不会阻塞读”,相反的例子是,基于S2PL的系统在写操作发生时会阻塞相应对象上的读操作,因为写入者获取了对象上的排他锁。 PostgreSQL和一些RDBMS使用一种MVCC的变体,名曰快照隔离(Snapshot Isolation,SI)。
一些RDBMS(例如Oracle)使用回滚段来实现快照隔离SI。当写入新数据对象时,旧版本对象先被写入回滚段,随后用新对象覆写至数据区域。 PostgreSQL使用更简单的方法:新数据对象被直接插入到相关表页中。读取对象时,PostgreSQL根据可见性检查规则(visibility check rules),为每个事务选择合适的对象版本作为响应。
SI中不会出现在ANSI SQL-92标准中定义的三种异常:脏读,不可重复读和幻读。但SI无法实现真正的可串行化,因为在SI中可能会出现串行化异常:例如写偏差(write skew)和只读事务偏差(Read-only Transaction Skew)。需要注意的是:ANSI SQL-92标准中可串行化的定义与现代理论中的定义并不相同。为了解决这个问题,PostgreSQL从9.1版本之后添加了可串行化快照隔离(SSI,Serializable Snapshot Isolation),SSI可以检测串行化异常,并解决这种异常导致的冲突。因此,9.1版本之后的PostgreSQL提供了真正的SERIALIZABLE
隔离等级(此外SQL Server也使用SSI,而Oracle仍然使用SI)。
本章包括以下四个部分:
第1部分:第5.1~5.3节。
这一部分介绍了理解后续部分所需的基本信息。
第5.1和5.2节分别描述了事务标识和元组结构。第5.3节展示了如何插入,删除和更新元组。
第2部分:第5.4~5.6节。
这一部分说明了实现并发控制机制所需的关键功能。
第5.4,5.5和5.6节描述了提交日志(clog),分别介绍了事务状态,事务快照和可见性检查规则。
第3部分:第5.7~5.9节。
这一部分使用具体的例子来介绍PostgreSQL中的并发控制。
这一部分说明了如何防止ANSI SQL标准中定义的三种异常。第5.7节描述了可见性检查,第5.8节介绍了如何防止丢失更新,第5.9节简要描述了SSI。
第4部分:第5.10节。
这一部分描述了并发控制机制持久运行所需的几个维护过程。维护过程主要通过清理过程(vacuum processing)进行,清理过程将在第6章详细阐述。
并发控制包含着很多主题,本章重点介绍PostgreSQL独有的内容。故这里省略了锁模式与死锁处理的内容(相关信息请参阅官方文档)。
PostgreSQL中的事务隔离等级
PostgreSQL实现的事务隔离等级如下表所示:
隔离等级 脏读 不可重复读 幻读 串行化异常 读已提交 不可能 可能 可能 可能 可重复读[1] 不可能 不可能 PG中不可能,见5.7.2小节 但ANSI SQL中可能 可能 可串行化 不可能 不可能 不可能 不可能 [1]:在9.0及更早版本中,该级别被当做
SERIALIZABLE
,因为它不会出现ANSI SQL-92标准中定义的三种异常。 但9.1版中SSI的实现引入了真正的SERIALIZABLE
级别,该级别已被改称为REPEATABLE READ
。PostgreSQL对DML(
SELECT, UPDATE, INSERT, DELETE
等命令)使用SSI,对DDL(CREATE TABLE
等命令)使用2PL。
5.1 事务标识
每当事务开始时,事务管理器就会为其分配一个称为事务标识(transaction id, txid)的唯一标识符。 PostgreSQL的txid
是一个32位无符号整数,总取值约42亿。在事务启动后执行内置的txid_current()
函数,即可获取当前事务的txid
,如下所示。
1 2 3 4 5 6 7 | 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
属于“未来”,且对于txid=100
的事务而言都是不可见(invisible)的;小于100的txid
属于“过去”,且对该事务可见,如图5.1(a)所示。
图5.1 PostgreSQL中的事务标识
因为txid
在逻辑上是无限的,而实际系统中的txid
空间不足(4字节取值空间约42亿),因此PostgreSQL将txid
空间视为一个环。对于某个特定的txid
,其前约21亿个txid
属于过去,而其后约21亿个txid
属于未来。如图5.1(b)所示。
所谓的txid
回卷问题将在5.10.1节中介绍。
请注意,
txid
并非是在BEGIN
命令执行时分配的。在PostgreSQL中,当执行BEGIN
命令后的第一条命令时,事务管理器才会分配txid
,并真正启动其事务。
5.2 元组结构
可以将表页中的堆元组分为两类:普通数据元组与TOAST元组。本节只会介绍普通元组。
堆元组由三个部分组成,即HeapTupleHeaderData
结构,空值位图,以及用户数据,如图5.2所示。
图5.2 元组结构
HeapTupleHeaderData
结构在src/include/access/htup_details.h
中定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | typedef struct HeapTupleFields { TransactionId t_xmin; /* 插入事务的ID */ TransactionId t_xmax; /*删除或锁定事务的ID*/ union { CommandId t_cid; /* 插入或删除的命令ID */ TransactionId t_xvac; /* 老式VACUUM FULL的事务ID */ } t_field3; } HeapTupleFields; typedef struct DatumTupleFields { int32 datum_len_; /* 变长头部长度*/ int32 datum_typmod; /* -1或者是记录类型的标识 */ Oid datum_typeid; /* 复杂类型的OID或记录ID */ } DatumTupleFields; typedef struct HeapTupleHeaderData { union { HeapTupleFields t_heap; DatumTupleFields t_datum; } t_choice; ItemPointerData t_ctid; /* 当前元组,或更新元组的TID */ /* 下面的字段必需与结构MinimalTupleData相匹配! */ uint16 t_infomask2; /* 属性与标记位 */ uint16 t_infomask; /* 很多标记位 */ uint8 t_hoff; /* 首部+位图+填充的长度 */ /* ^ - 23 bytes - ^ */ bits8 t_bits[1]; /* NULL值的位图 —— 变长的 */ /* 本结构后面还有更多数据 */ } HeapTupleHeaderData; typedef HeapTupleHeaderData *HeapTupleHeader; |
虽然HeapTupleHeaderData
结构包含七个字段,但后续部分中只需要了解四个字段即可。
t_xmin
保存插入此元组的事务的txid
。t_xmax
保存删除或更新此元组的事务的txid
。如果尚未删除或更新此元组,则t_xmax
设置为0,即无效。t_cid
保存命令标识(command id, cid),cid
意思是在当前事务中,执行当前命令之前执行了多少SQL命令,从零开始计数。例如,假设我们在单个事务中执行了三条INSERT
命令BEGIN;INSERT;INSERT;INSERT;COMMIT;
。如果第一条命令插入此元组,则该元组的t_cid
会被设置为0。如果第二条命令插入此元组,则其t_cid
会被设置为1,依此类推。t_ctid
保存着指向自身或新元组的元组标识符(tid
)。如第1.3节中所述,tid
用于标识表中的元组。在更新该元组时,其t_ctid
会指向新版本的元组;否则t_ctid
会指向自己。
5.3 元组的增删改
本节会介绍元组的增删改过程,并简要描述用于插入与更新元组的自由空间映射(Free Space Map, 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)
,指向自身,因为这是该元组的最新版本。
pageinspect
PostgreSQL自带了一个第三方贡献的扩展模块
pageinspect
,可用于检查数据库页面的具体内容。
123456789101112 testdb=# CREATE EXTENSION pageinspect;CREATE EXTENSIONtestdb=# CREATE TABLE tbl (data text);CREATE TABLEtestdb=# INSERT INTO tbl VALUES(A);INSERT 0 1testdb=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctidFROM 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所示。
图5.5 删除元组
假设Tuple_1
被txid=111
的事务删除。在这种情况下,Tuple_1
的首部字段会依以下步骤设置。
Tuple_1
:
t_xmax
被设为111。
如果txid=111
的事务已经提交,那么Tuple_1
就不是必需的了。通常不需要的元组在PostgreSQL中被称为死元组(dead tuple)。
死元组最终将从页面中被移除。清除死元组的过程被称为清理(VACUUM)过程,第6章将介绍清理过程。
5.3.3 更新
在更新操作中,PostgreSQL在逻辑上实际执行的是删除最新的元组,并插入一条新的元组(图5.6)。
图5.6 两次更新同一行
假设由txid=99
的事务插入的行,被txid=100
的事务更新两次。
当执行第一条UPDATE
命令时,Tuple_1
的t_xmax
被设为txid 100
,在逻辑上被删除;然后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
能提供特定表或索引上的空闲空间信息。以下查询列出了特定表中每个页面的空闲率。
1234567891011121314 testdb=# CREATE EXTENSION pg_freespacemap;CREATE EXTENSIONtestdb=# SELECT *, round(100 * avail/8192 ,2) as "freespace ratio"FROM pg_freespace(accounts);blkno | avail | freespace ratio-------+-------+-----------------0 | 7904 | 96.001 | 7520 | 91.002 | 7136 | 87.003 | 7136 | 87.004 | 7136 | 87.005 | 7136 | 87.00....
5.4 提交日志(clog)
PostgreSQL在提交日志(Commit Log, clog)中保存事务的状态。提交日志(通常称为clog)分配于共享内存中,并用于事务处理过程的全过程。
本节将介绍PostgreSQL中事务的状态,clog的工作方式与维护过程。
5.4.1 事务状态
PostgreSQL定义了四种事务状态,即:IN_PROGRESS
,COMMITTED
,ABORTED
和SUB_COMMITTED
。
前三种状态涵义显而易见。例如当事务正在进行时,其状态为IN_PROGRESS
,依此类推。
SUB_COMMITTED
状态用于子事务,本文省略了与子事务相关的描述。
5.4.2 提交日志如何工作
提交日志(下称clog)在逻辑上是一个数组,由共享内存中一系列8KB页面组成。数组的序号索引对应着相应事务的标识,而其内容则是相应事务的状态。clog的工作方式如图5.7所示。
图5.7 clog如何工作
T1:
txid 200
提交;txid 200
的状态从IN_PROGRESS
变为COMMITTED
。 T2:txid 201
中止;txid 201
的状态从IN_PROGRESS
变为ABORTED
。
txid
不断前进,当clog空间耗尽无法存储新的事务状态时,就会追加分配一个新的页面。
当需要获取事务的状态时,PostgreSQL将调用相应内部函数读取clog,并返回所请求事务的状态。(参见第5.7.1节中的提示位(Hint Bits))
5.4.3 提交日志的维护
当PostgreSQL关机或执行存档过程时,clog数据会写入至pg_clog
子目录下的文件中(注意在10版本中,pg_clog
被重命名为pg_xact
)。这些文件被命名为0000
,0001
等等。文件的最大尺寸为256 KB。例如当clog使用八个页面时,从第一页到第八页的总大小为64 KB,这些数据会写入到文件0000
(64 KB)中;而当clog使用37个页面时(296 KB),数据则会写入到0000
和0001
两个文件中,其大小分别为256 KB和40 KB。
当PostgreSQL启动时会加载存储在pg_clog
(pg_xact
)中的文件,用其数据初始化clog。
clog的大小会不断增长,因为只要clog一填满就会追加新的页面。但并非所有数据都是必需的。第6章中描述的清理过程会定期删除这些不需要的旧数据(clog页面和文件),有关删除clog数据的详情请参见第6.4节。
5.5 事务快照
事务快照(transaction snapshot)是一个数据集,存储着某个特定事务在某个特定时间点所看到的事务状态信息:哪些事务处于活跃状态。这里活跃状态意味着事务正在进行中,或还没有开始。
事务快照在PostgreSQL内部的文本表示格式定义为100:100:
。举个例子,这里100:100:
意味着txid < 100
的事务处于非活跃状态,而txid ≥ 100
的事务处于活跃状态。下文都将使用这种便利形式来表示。如果读者还不熟悉这种形式,请参阅下文。
内置函数
txid_current_snapshot
及其文本表示函数
txid_current_snapshot
显示当前事务的快照。
12345 testdb=# SELECT txid_current_snapshot();txid_current_snapshot-----------------------100:104:100,102(1 row)
txid_current_snapshot
的文本表示是xmin:xmax:xip_list
,各部分描述如下。
xmin
最早仍然活跃的事务的
txid
。所有比它更早的事务txid < xmin
要么已经提交并可见,要么已经回滚并生成死元组。
xmax
第一个尚未分配的
txid
。所有txid ≥ xmax
的事务在获取快照时尚未启动,因而其结果对当前事务不可见。
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)所示,此快照表示:
- 因为
xmin
为100,因此txid < 100
的事务是非活跃的- 因为
xmax
为100,因此txid ≥ 100
的事务是活跃的第二个例子是
100:104:100,102
,如图5.8(b)所示,此快照表示:
txid < 100
的事务不活跃。txid ≥ 104
的事务是活跃的。txid
等于100和102的事务是活跃的,因为它们在xip_list
中,而txid
等于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)
正在进行中。因此无法从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
,提交日志clog,以及已获取的事务快照。这些规则太复杂,无法详细解释,故本书只列出了理解后续内容所需的最小规则子集。在下文中省略了与子事务相关的规则,并忽略了关于t_ctid
的讨论,比如我们不会考虑在同一个事务中对一条元组多次重复更新的情况。
所选规则有十条,可以分类为三种情况。
5.6.1 t_xmin
的状态为ABORTED
t_xmin
状态为ABORTED
的元组始终不可见(规则1),因为插入此元组的事务已中止。
1 2 3 4 | /* 创建元组的事务已经中止 */ Rule 1: IF t_xmin status is ABORTED THEN RETURN Invisible END IF |
该规则明确表示为以下数学表达式。
- 规则1:
If Status(t_xmin) = ABORTED ⇒ Invisible
5.6.2 t_xmin
的状态为IN_PROGRESS
t_xmin
状态为IN_PROGRESS
的元组基本上是不可见的(规则3和4),但在一个条件下除外。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /* 创建元组的事务正在进行中 */ 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 /* 这条元组被当前事务自己创建后又删除掉了,故不可见 */ 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 ≠ 0
,则该元组是不可见的,因为它已被当前事务更新或删除(规则3)。
例外是,当前事务插入此元组且t_xmax
无效(t_xmax = 0
)的情况。 在这种情况下,此元组对当前事务中可见(规则2)。
- 规则2:
If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD ⇒ Visible
- 规则3:
If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax ≠ INVAILD ⇒ Invisible
- 规则4:
If Status(t_xmin) = IN_PROGRESS ∧ t_xmin ≠ current_txid ⇒ Invisible
5.6.3 t_xmin
的状态为COMMITTED
t_xmin
状态为COMMITTED
的元组是可见的(规则 6,8和9),但在三个条件下除外。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /* 创建元组的事务已经提交 */ 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
,或者t_xmax
对应事务已经中止,相应元组可见。三个例外条件及规则8与规则9的描述如下。
第一个例外情况是t_xmin
在获取的事务快照中处于活跃状态(规则5)。在这种情况下,这条元组是不可见的,因为t_xmin
应该被视为正在进行中(取快照时创建该元组的事务尚未提交,因此对于REPEATABLE READ
以及更高隔离等级而言,即使在判断时创建该元组的事务已经提交,但其结果仍然不可见)。
第二个例外情况是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:
If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active ⇒ Invisible
- 规则6:
If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) ⇒ Visible
- 规则7:
If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid ⇒ Invisible
- 规则8:
If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax ≠ current_txid ⇒ Visible
- 规则9:
If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active ⇒ Visible
- 规则10:
If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) ≠ active ⇒ Invisible
5.7 可见性检查
本节描述了PostgreSQL执行可见性检查的流程。可见性检查(Visiblity Check),即如何为给定事务挑选堆元组的恰当版本。本节还介绍了PostgreSQL如何防止ANSI SQL-92标准中定义的异常:脏读,可重读和幻读。