5.7 可见性检查

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

5.7.1 可见性检查

图5.10中的场景描述了可见性检查的过程。

图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=200201的事务。txid=200的事务的隔离级别是READ COMMITTED,而txid=201的事务的隔离级别是READ COMMITTEDREPEATABLE 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)级别中不会发生不可重复读。
  • 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在内部提供了三个函数TransactionIdIsInProgressTransactionIdDidCommitTransactionIdDidAbort,用于获取事务的状态。这些函数被设计为尽可能减少对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标记,表示创建这条元组的事务已经提交了。 如果已经设置了提示位,则不再需要调用TransactionIdDidCommitTransactionIdDidAbort来获取事务状态了。 因此PostgreSQL能高效地检查每个元组t_xmint_xmax对应事务的状态。

5.7.2 PostgreSQL可重复读等级中的幻读

ANSI SQL-92标准中定义的REPEATABLE READ隔离等级允许出现幻读(Phantom Reads) , 但PostgreSQL实现的REPEATABLE READ隔离等级不允许发生幻读。 在原则上,快照隔离中不允许出现幻读。

假设两个事务Tx_ATx_B同时运行。 它们的隔离级别分别为READ COMMITTEDREPEATABLE 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_ATx_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) |