天天看點

PostgreSQL中同樣是别人送出的資料,為什麼你能看到,我卻看不到?

原創: Aken DB印象

文章連結:

https://mp.weixin.qq.com/s/OkJaWbzcXcJtzSCOFnqeXQ

文章作為DB的學習體會,若有錯誤歡迎指導。

一、環境介紹

作業系統:CentOS Linux release 7.6.1810 (Core)

DB版本:PostgreSQL -11.5 on x86_64-pc-linux-gnu

二、問題描述

同一個執行個體運作的3個session,在T2時刻session 1向表table01插入一行資料之後,session 2和session 3兩個會話執行相同的SQL查詢的結果不一樣。如下:

PostgreSQL中同樣是别人送出的資料,為什麼你能看到,我卻看不到?

上圖中,session 2查到的是2行記錄,session 3卻隻有1條記錄。為什麼session 2能看到session 1新插入的記錄,而session 3卻看不到呢?這種情況是在什麼場景下發生的呢?

三、相關理論知識回顧

如果有熟悉事務隔離級别的朋友可能已經想到大概的原因。關于事務的隔離級别的介紹,有興趣的可以檢視上一篇文章。

PostgreSQL的事務隔離級别介紹及更改

在說明原因之前,這裡先介紹一下PostgreSQL中取名為“transaction snapshot”這個東西,即事務快照。

至于什麼是事務快照,以及為什麼需要事務快照,我在官方文檔中暫時沒有看到具體的描述。

下面是個人的了解,不代表官方:

平時我們執行SQL資料讀取的時候,實際上讀取的是一種狀态資料,transaction snapshot本義上指是某個時刻事務的快照,實質代表的是具體時刻具體事務下資料的狀态。

既然是狀态,那麼可能就有目前狀态、上一個狀态、下一個狀态一說。資料庫中所說的事務可看作是将資料從上一個狀态進入到另一個狀态的機關。

這是資料庫中的“詞典”,了解起來比較幹澀,我們可以對應到人類詞典中比較容易了解的三個階段:過去的、目前的、未來的。

是以,我對事務快照的了解為三個階段:一個transaction snapshot将事務劃分為過去的、目前的、未來的三個區域。

比較友好的是,PostgreSQL官方給我們提供了一個擷取事務快照的函數:txid_current_snapshot。下面是官網對txid_current_snapshot函數輸出結果的原文解析:

Table 9.75. Snapshot Components for PostgreSQL-12

詳細介紹見:

https://www.postgresql.org/docs/current/functions-info.html
  • xmin,目前處于active狀态的最小事務編号;
  • xmax,未來産生的事務中,第一個将被配置設定的事務編号;
  • xip_list,目前處于active 狀态的事務清單(包括in progress和future狀态的事務),其餘為inactive。

如下,檢視目前時刻事務快照:

(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
639:642:639,641 <<<事務快照文本格式:xmin:xmax:xip_list           

1.xmin=639,表示目前時刻快照中最小的是639這個事務。小于該編号的事務都已經終止(送出、復原或異常終止),這些事務屬于“過去的”範圍區域。

2.xmax=642,表示将來新事務産生時配置設定到的第一個事務編号txid,大于等于642的事務未産生,屬于“将來的”範圍區域。

3.xip_list=(639,641),表示該快照時刻639和641這兩個事務正處于active狀态,屬于“目前的”範圍區域。

畫成圖就是下面這個樣子:

PostgreSQL中同樣是别人送出的資料,為什麼你能看到,我卻看不到?

transaction snapshot examples

四、原因分析

在PostgreSQL中,送出讀(或者叫讀送出)read committed事務隔離級别下,session中同一事務的每條SQL執行的時候都會自動去讀取目前時刻的事務快照;而在repeatable read級别下,session中同一事務隻會在事務開始的第一個SQL擷取一次事務快照。

因為read committed級别下,同一事務中不同時刻的SQL擷取的快照可能不一樣,是以讀到的資料可能會不一樣。

而repeatable read在整個事務周期隻擷取一次事務快照,是以同一事務内所有SQL使用的快照都是一緻的,是以可以實作重複讀,規避了幻讀的産生。

pg預設的事務隔離級别transaction isolation為read committed。這是上面文章開頭session 2中read committed事務級别下産生幻讀的原因,也是session 3中repeatable read可以實作重複讀的原因。

請原諒我在文章開頭故意将會話的事務隔離級别忽略,目的是為了引導大家可以一起思考。

說到這裡,MySQL的朋友可能覺得PostgreSQL中transaction snapshot和MySQL中的一緻性視圖Read view有點像。

是以,對于文章開頭的問題:

1.對于session 2和session 3的結果來說,上述的問題并非因為資料的不一緻,而是因為不同的事務隔離級别讀取的結果有所差別。

2.對于session 2來說,在同一個事務裡面執行相同的查詢語句前後得到的結果不一緻,這種情況叫幻讀。

什麼是幻讀? 下面是官方的原文解析:

phantom read

A transaction re-executes a query returning a set of rows that satisfy a search condition and finds that the set of rows satisfying the condition has changed due to another recently-committed transaction.

大概意思指:

在一個事務中相同的SQL查詢條件前後讀取到的結果不一緻,原因是後者讀取到了其他事務中新送出的資料。

這個問題其實在PostgreSQL-12官方文檔中有所提示,pg中repeatable read隔離級别下是不會出現幻讀的。如下圖示紅處所示:

PostgreSQL中同樣是别人送出的資料,為什麼你能看到,我卻看不到?

PostgreSQL-12事務隔離級别

為什麼在PostgreSQL中的repeatable read下是Allowed,but not in PG呢?

這正是因為事務快照的作用。下面将文章開始時的例子進行充分的示範。

五、場景示範:送出讀、可重複讀事務快照對比

下面針對read committed和repeatable read兩種事務隔離模式下的事務快照進行對比測試,例子如下:

PostgreSQL中同樣是别人送出的資料,為什麼你能看到,我卻看不到?

1.T0時間段:

session 1在預設情況下開啟事務,txid=666。

session 2在read committed隔離模式下開啟事務,txid=674;

session 3在可重複讀repeatable read隔離模式下開啟事務,txid=675;

session 4開啟事務txid=676(略)。

1)事務開始前table01中隻有一行記錄:tuple 1

(postgres@[local]:5432)[akendb01]#select * from table01;
 id | name
----+--------
 1 | aken01
(1 row)
(postgres@[local]:5432)[akendb01]#           

2)session 1在預設送出讀模式下開啟事務,事務編号txid=666。

(postgres@[local]:5432)[akendb01]#begin;
BEGIN
(postgres@[local]:5432)[akendb01]#show default_transaction_isolation;
 default_transaction_isolation
-------------------------------
 read committed
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select txid_current();
 txid_current
--------------
 666
(1 row)
(postgres@[local]:5432)[akendb01]#           

3)session 2:在送出讀隔離級别下開啟事務,事務編号txid=674。

(postgres@[local]:5432)[akendb01]#start transaction isolation level read committed;
START TRANSACTION
(postgres@[local]:5432)[akendb01]#select txid_current();
 txid_current
--------------
 674
(1 row)           

4)session 3:在可重複讀隔離級别下開啟事務,事務編号txid=675

(postgres@[local]:5432)[akendb01]#start transaction isolation level repeatable read;
START TRANSACTION
(postgres@[local]:5432)[akendb01]#select txid_current();
 txid_current
--------------
 675
(1 row)           

5)session 4:配置設定一個事務txid=676

(postgres@[local]:5432)[akendb01]#select txid_current();
 txid_current
--------------
 676
(1 row)           

2.T1時刻,session 1、2、3擷取目前事務快照,并讀取table01的記錄。

1)session 1:讀取到的事務快照為'666:676:674,675',讀取表的記錄數為1行。

(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
 txid_current_snapshot
-----------------------
666:676:674,675   <<< 實際上txid=676在session 4已經配置設定,這個和官網将xmax解析為将來産生的第一個事務有沖突,pg擷取事務快照時最後一個txid是否會滞後?
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
(1 rows)
(postgres@[local]:5432)[akendb01]#           

2)session 2:讀取到的事務快照為'666:676:666,675',讀取表的記錄數為1行。

(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
 txid_current_snapshot
-----------------------
 666:676:666,675
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
(1 rows)
(postgres@[local]:5432)[akendb01]#           

3)session 3:讀取到的事務快照為'666:676:666,674',讀取表的記錄數為1行。

(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
 txid_current_snapshot
-----------------------
 666:676:666,674
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
(1 rows)
(postgres@[local]:5432)[akendb01]#           

3.T2時刻,session 1往table01插入一行記錄并commit送出,session 1、2、3讀取table01的記錄。

1)session 1在事務txid=666中擷取的事務快照為'674:676:674,675',檢視結果中可以看到自己新插入的tuple 2。

(postgres@[local]:5432)[akendb01]#insert into table01 values(2,'aken02');
INSERT 0 1
(postgres@[local]:5432)[akendb01]#commit;
COMMITTED
(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
674:676:674,675 <<< 事務666已送出,session 1事務快照改變,xmin=674
(1 row)
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
2 | aken02
(2 rows)
(postgres@[local]:5432)[akendb01]#           

2)session 2:

session 2在事務txid=674中擷取到的快照為'674:676:675'和T1時刻不同,能看到事務txid=666新插入的tuple 2,産生幻讀。

(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
674:676:675  <<< session 1的事務666<xmin,txid=666變成過去狀态的inactive事務,可見。
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
2 | aken02
(2 rows)
(postgres@[local]:5432)[akendb01]#           

3)session 3:

session 3在事務txid=675中擷取的事務快照依舊為'666:676:666,674',和T1時刻的保持一緻,看不到事務txid=666新插入的tuple 2,無幻讀産生。

(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
666:676:666,674  <<<盡管session 1事務txid=666已送出,但在repeatable read隔離級别下仍然當作active處理,不可見
(1 row)
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
(1 rows)
(postgres@[local]:5432)[akendb01]#           

4.T3時間段

session 2、session 3事務結束,session 1、2、3讀取到的事務快照都為“676:676:”,且查詢結果相同。

(postgres@[local]:5432)[akendb01]#select txid_current_snapshot();
txid_current_snapshot
-----------------------
676:676: <<<xip_list為空,xmin=xmax,表示目前快照無活躍事務,未來産生的第一個事務為676.
(1 row)
(postgres@[local]:5432)[akendb01]#
(postgres@[local]:5432)[akendb01]#select * from table01;
id | name
----+--------
1 | aken01
2 | aken02
(2 rows)