
原創: 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查詢的結果不一樣。如下:

上圖中,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 <<
- 1.xmin=639,表示當前時刻快照中最小的是639這個事務。小于該編號的事務都已經終止(提交、回滾或異常終止),這些事務屬于“過去的”范圍區域。
- 2.xmax=642,表示將來新事務產生時分配到的第一個事務編號txid,大于等于642的事務未產生,屬于“將來的”范圍區域。
- 3.xip_list=(639,641),表示該快照時刻639和641這兩個事務正處于active狀態,屬于“當前的”范圍區域。
畫成圖就是下面這個樣子:

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-12事務隔離級別
為什么在PostgreSQL中的repeatable read下是Allowed,but not in PG呢?
這正是因為事務快照的作用。下面將文章開始時的例子進行充分的演示。
五、場景演示:提交讀、可重復讀事務快照對比
下面針對read committed和repeatable read兩種事務隔離模式下的事務快照進行對比測試,例子如下:

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 | aken012 | 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
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 <<
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: <<
