本节描述了PostgreSQL执行可见性检查的流程。可见性检查(Visiblity Check),即如何为给定事务挑选堆元组的恰当版本。本节还介绍了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"
。
Rule 6(Tuple_1) ⇒ Status(t_xmin:199) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible
创建元组Tuple_1
的事务199已经提交,且该元组并未被标记删除,因此根据规则6,对当前事务可见。testdb=# -- txid 200 testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
testdb=# -- txid 201 testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
T5的SELECT
命令
首先来看一下由txid=200
的事务所执行的SELECT
命令。根据规则7,Tuple_1
不可见,根据规则2,Tuple_2
可见;因此该SELECT
命令返回"Hyde"
。
Rule 7(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = IN_PROGRESS ∧ t_xmax:200 = current_txid:200 ⇒ Invisible
创建元组Tuple_1
的事务199已经提交,且该元组被当前事务标记删除,根据规则7,Tuple_1
对当前事务不可见。Rule 2(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧ t_xmin:200 = current_txid:200 ∧ t_xmax = INVAILD ⇒ Visible
创建元组Tuple_2
的事务200正在进行,而且就是当前事务自己,根据规则2,Tuple_2
对当前事务可见。testdb=# -- txid 200 testdb=# SELECT * FROM tbl; name ------ Hyde (1 row)
另一方面,在由txid=201
的事务所执行的SELECT
命令中,Tuple_1
基于规则8确定可见,而Tuple_2
基于规则4不可见;因此该SELECT
命令返回"Jekyll"
。
Rule 8(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = IN_PROGRESS ∧ t_xmax:200 ≠ current_txid:201 ⇒ Visible
元组Tuple_1
由已提交事务199创建,由活跃事务200标记删除,但删除效果对当前事务201不可见。因此根据规则8,Tuple_1
可见。Rule 4(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧ t_xmin:200 ≠ current_txid:201 ⇒ Invisible
元组Tuple_2
由活跃事务200创建,且不是由当前事务自己创建的,故根据规则4,Tuple_2
不可见。testdb=# -- txid 201 testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
如果更新的元组在本事务提交之前被其他事务看见,这种现象被称为脏读(Dirty Reads) ,也称为写读冲突(wr-conflicts) 。 但如上所示,PostgreSQL中任何隔离级别都不会出现脏读。
T7的SELECT
命令
在下文中,描述了T7的SELECT
命令在两个隔离级别中的行为。
首先来研究txid=201
的事务处于READ COMMITTED
隔离级别时的情况。 在这种情况下,txid=200
的事务被视为已提交,因为在这个时间点获取的事务快照是201:201:
。因此Tuple_1
根据规则10不可见,Tuple_2
根据规则6可见,SELECT
命令返回"Hyde"
。
Rule 10(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = COMMITTED ∧ Snapshot(t_xmax:200) ≠ active ⇒ Invisible
元组Tuple_1
由已提交事务199创建,由非活跃的已提交事务200标记删除,Tuple_1
按照规则10不可见。Rule 6(Tuple_2): Status(t_xmin:200) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible
元组Tuple_2
由已提交事务200创建,且未被标记为删除,故Tuple_2
按照规则6可见。testdb=# -- txid 201 (READ COMMITTED) testdb=# SELECT * FROM tbl; name ------ Hyde (1 row)
- 这里需要注意,事务201中的
SELECT
命令,在txid=200
的事务提交前后中时的执行结果是不一样的,这种现象通常被称作不可重复读(Non-Repeatable Read) 。 - 相反的是,当
txid=201
的事务处于REPEATABLE READ
级别时,即使在T7时刻txid=200
的事务实际上已经提交,它也必须被视作仍在进行,因而获取到的事务快照是200:200:
。 根据规则9,Tuple_1
是可见的,根据规则5,Tuple_2
不可见,所以最后SELECT
命令会返回"Jekyll"
。 请注意在REPEATABLE READ
(和SERIALIZABLE
)级别中不会发生不可重复读。
- 这里需要注意,事务201中的
Rule9(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = COMMITTED ∧ Snapshot(t_xmax:200) = active ⇒ Visible
元组Tuple_1
由已提交事务199创建,由已提交事务200标记删除,但因为事务200位于当前事物的活跃事务快照中(也就是在当前事物201开始执行并获取事务级快照时,事物200还未提交),因此删除对当前事务尚未生效,根据规则9,Tuple_1
可见。Tuple_1
按照规则10不可见。Rule5(Tuple_2): Status(t_xmin:200) = COMMITTED ∧ Snapshot(t_xmin:200) = active ⇒ Invisible
元组Tuple_2
由已提交事务200创建,但该事务在本事务快照中属于活跃事务(即在本事务开始前还未提交),因此事务200的变更对本事务尚不可见,按照规则5,Tuple_2
不可见。testdb=# -- txid 201 (REPEATABLE READ) testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
提示位(Hint Bits)
PostgreSQL在内部提供了三个函数
TransactionIdIsInProgress
,TransactionIdDidCommit
和TransactionIdDidAbort
,用于获取事务的状态。这些函数被设计为尽可能减少对clog的频繁访问。 尽管如此,如果在检查每条元组时都执行这些函数,那这里很可能会成为一个性能瓶颈。为了解决这个问题,PostgreSQL使用了提示位(hint bits) ,如下所示。
#define HEAP_XMIN_COMMITTED 0x0100 /* 元组xmin对应事务已提交 */ #define HEAP_XMIN_INVALID 0x0200 /* 元组xmin对应事务无效/中止 */ #define HEAP_XMAX_COMMITTED 0x0400 /* 元组xmax对应事务已提交 */ #define HEAP_XMAX_INVALID 0x0800 /* 元组xmax对应事务无效/中止 */
在读取或写入元组时,PostgreSQL会择机将提示位设置到元组的
t_informask
字段中。 举个例子,假设PostgreSQL检查了元组的t_xmin
对应事务的状态,结果为COMMITTED
。 在这种情况下,PostgreSQL会在元组的t_infomask
中置位一个HEAP_XMIN_COMMITTED
标记,表示创建这条元组的事务已经提交了。 如果已经设置了提示位,则不再需要调用TransactionIdDidCommit
和TransactionIdDidAbort
来获取事务状态了。 因此PostgreSQL能高效地检查每个元组t_xmin
和t_xmax
对应事务的状态。
5.7.2 PostgreSQL可重复读等级中的幻读
ANSI SQL-92标准中定义的REPEATABLE READ
隔离等级允许出现幻读(Phantom Reads) , 但PostgreSQL实现的REPEATABLE READ
隔离等级不允许发生幻读。 在原则上,快照隔离中不允许出现幻读。
假设两个事务Tx_A
和Tx_B
同时运行。 它们的隔离级别分别为READ COMMITTED
和REPEATABLE READ
,它们的txid
分别为100和101。两个事务一前一后接连开始,首先Tx_A
插入一条元组,并提交。 插入的元组的t_xmin
为100。接着,Tx_B
执行SELECT
命令;但根据规则5,Tx_A
插入的元组对Tx_B
是不可见的。因此不会发生幻读。
Rule5(new tuple): Status(t_xmin:100) = COMMITTED ∧ Snapshot(t_xmin:100) = active ⇒ Invisible
新元组由已提交的事务Tx_A
创建,但Tx_A
在Tx_B
的事务快照中处于活跃状态,因此根据规则5,新元组对Tx_B
不可见。 |Tx_A: txid = 100
|Tx_B: txid = 101
| | ----------------------------------------------------- | ------------------------------------------------------ | |START TRANSACTION ISOLATION LEVEL READ COMMITTED;
|START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
| |INSERT tbl(id, data)
| | |COMMIT;
| | | |SELECT * FROM tbl WHERE id=1;
| | |(0 rows)
|