PostgreSQL串列化隔離級別(SSI)的能力與實現

PostgreSQL串列化隔離級別(SSI)的能力與實現

PostgreSQL9.1是第一個採用Serializable Snapshot Isolation(SSI)實現串列化隔離級別的生產級資料庫。本文是對相關論文與文檔的學習筆記,目標是分析與學習SSI的設計,以及在PG中的實現與優化。本文首先介紹了隔離級別以及實現其的兩個基本並發控制機制、給出了PG的SI未達到串列化的案例並分析原因與給出直觀的解決思路,其次闡述了SSI的技術思路與關鍵環節,最後就PG下SSI的實現與優化進行了分析。

1.SI的異常現象與直觀分析

1.1隔離性與其實現機制

隔離性

隔離性是事務的關鍵特徵之一,用於保證並發訪問時的數據一致性。如果一個並發操作序列與一個串列序列的結果相同,則認為此並發操作的結果是一致性(資料庫只在此層面保證一致,語義的一致由應用保證)。兩個事務對相同數據對象的寫寫(WW)、寫讀(WR)、讀寫(RW)操作存在衝突,讀讀(RR)操作無衝突。

對於衝突的序列,需要資料庫進行並發控制,使其不發生(或者發生後回滾)。鎖機制和Snapshot機制是實現該並發控制的基礎設施。

S2PL(謂詞鎖)可實現串列化,但卻有較大的性能負擔。為了平衡性能與正確性,隔離性被分為分為多個等級,每個等級中鎖與Snapshot的處理流程有差異,低等級隔離級別的隔離能力弱但性能負擔小。

基於鎖的並發控制

鎖模式:讀操作申請讀鎖、寫操作申請寫鎖

衝突處理:相同對象的讀寫、寫讀、寫寫會發生衝突,衝突則等待

鎖的粒度:行、頁、表、謂詞(index range)

鎖的持續時間:短-操作結束時結束釋放,長-事務結束時釋放

2PL, S2PL

基於鎖定義的隔離級別

  • lock based read committed:W操作,長寫鎖,數據對象鎖;R操作,短讀鎖,數據對象鎖
  • lock based Repeatable read:W操作,長寫鎖,數據對象鎖;R操作,長讀鎖,數據對象鎖
  • lock based Serializable :W操作,長寫鎖,數據對象鎖+謂詞鎖;R操作,長讀鎖,數據對象鎖+謂詞鎖

基於Snapshot的並發控制

記錄的MVCC結構:PG的記錄鏈表與Innodb的回滾區兩種方式。

Snapshot的數據結構-xmin、xmax、list,含義-可以確定在獲取snapshot的時間點,某事務是否已完成。

Snapshot的獲取時機:每語句-即每條語句獲取一次,每事務-每個事務獲取一次(第一次訪問前)

操作流程:

  • 相同對象讀寫、寫讀不阻塞,寫寫阻塞
  • 讀操作:如果某事務在當前事務獲取snapshot時,已經結束,則某事務的操作結果(插入或刪除)對當前事務可見。
  • 寫操作:First Commit Win。實際採用寫鎖實現,寫操作加鎖,獲取寫鎖後流程與基於鎖的隔離級別不同。獲取寫鎖後的兩種流程:
    • 繼續加鎖型:找到最新的記錄版本,繼續加鎖。
    • 版本檢查型:如果獲取鎖的記錄被不可見,則回滾,否則繼續。

基於Snapshot的隔離級別

  • snapshot based read committed:W操作,長寫鎖,數據對象鎖。獲取snapshot-每語句,持續加鎖型。
  • snapshot based Repeatable:W操作,長寫鎖,數據對象鎖。獲取snapshot-每事務,版本檢查型。

資料庫產品中的隔離級別

  • PG/Oracle
    • read committed:snapshot based read committed
    • Repeatable read:snapshot based Repeatable
  • InnoDB
    • read committed:snapshot based read committed
    • Repeatable read:snapshot based Repeatable(使用繼續加鎖而不是版本檢查,不能避免更新丟失)
    • Serializable:lock based Serializable

1.2 異常案例

PG的snapshot based Repeatable read(下面簡稱SI)可以避免ansi標準中提出的三種具體的異常操作序列,但卻沒有達到串列化級別(SI<< Lock based Serializable)。InnoDB的lock based Serializable實現了串列化級別,可以避免PG的問題。

《A Critique of ANSI SQL Isolation Levels》中給出了異常操作序列的定義,這裡只看其中的6個案例。可以看出PG的SI未達到InnoDB的串列化級別。

  • 讀未提交A1:PG的SI可避免。
  • 不可重複讀A2:PG的SI可避免
  • 幻象A3:PG的SI可避免
  • 更新丟失P4:PG的SI可避免
  • 幻象A3B:PG的SI無法避免,InnoDB串列化可以避免
  • write skew A5B:PG的SI無法避免,InnoDB串列化可以避免

PostgreSQL

設置PostgreSQL隔離級別

SET SESSION CHARACTERISTICS AS TRANSACTION isolation level serializable;

讀未提交A1:PG的SI可避免。

不可重複讀A2:PG的SI可避免

幻象A3:PG的SI可避免

更新丟失P4:PG的SI可避免

幻象A3B:PG的SI無法避免

write skew A5B:PG的SI無法避免

InnoDB

設置隔離級別

set session transaction isolation level serializable;

幻象A3B:InnoDB串列化可以避免

write skew A5B:InnoDB串列化可以避免

1.3 異常的特點分析與直觀解決思路

SI中,事務內讀到的數據都是一致的(複雜情況也會錯誤,見後),因此可以避免A1、A2和A3,對於A3B和A5B,事務內部讀到的數據也是一致的,但是在外部整體上出現了不一致。

P4與A5B非常類似,都是根據先前的數據,進行寫操作,只是P4的讀與寫是相同對象,A3B的讀與寫是不同對象。P4沒有問題,因為寫操作有版本檢查,這正好可以處理讀到數據發生變化的情況,而A3B中讀寫不同對象,寫操作不能發現讀的數據發生變化。因此SI的讀,需要做登記工作(與lock manager類似,只是不阻塞),用於衝突的判斷。

A3B的操作序列中,讀操作是一個範圍。對這類讀操作登記,需要採用類似於謂詞鎖(range lock)的技術。

根據以上兩點,需要在事務提交時,判斷讀到的數據,是否發生了變化。當發生變化時,一定會導致不一致嗎?不一定,對於只有讀操作的事務,即使其讀到的數據發生了變化,也不一定會影響一致性。因此還需要額外的信息。

S1中讀寫不衝突,是通過讀寫同一數據的不同版本實現的。SI中兩個調度,R1W2(T1讀後T2寫),W2R1(W2先寫T1讀)是等價的,因為W2R1中事務1讀到的數據版本與R1W2相同。

基於鎖的串列化在處理A3B和A5B時,使用了死鎖檢測,發現了操作中的特定的依賴關係。基於SI的串列化需要的額外信息正是此類信息,需要在事務提交前,保存依賴並識別出與死鎖類似的結構,並進行衝突處理。

下面兩部分基於《serializable Snapshot Isolation in PostgreSQL》

2.SSI的技術路線

2.1隻讀事務異常案例與分析

論文中給出了一個違反直覺的只讀事務異常案例。應用為一個事務批處理系統,包括三個事務。receipts表記錄了產生的的receipt(可理解為訂單,多行記錄),control表記錄了一個批處理號(只有一行記錄)。T2(New Receipt)會定時將新的receipt插入receipts表,具體的信息欄位不用關心,只需要關注的批處理號欄位。批處理號為一個整數,該信息被儲存在control表中。T2插入receipts表前,首先會從control表中獲取批處理號,設置為插入記錄的批處理號欄位。T3(Close Batch)會定期更新control表,原有批處理號增一。T1(Report)是一個只讀事務,統計當前批處理號之前的那個批處理信息。

在T1、T2、T3串列執行時,T1每次執行的結果是相同的。而在異常案例中,T1會出現兩次讀到結果不同的異常。

出現異常的原因是,當前批處理號之前的批處理,必須已經完成,這個串列時存在的假設,在並行時被破壞了。在日常生活也會遇到這樣的情況,如統計企業的年度財務報表,不會在新年的第一天,而是要等到所有上一年度的業務全部完成後。

2.2 SI中的讀寫依賴關係

多版本中事務的依賴關係

Adya給出了一種表示多版本中事務讀寫依賴關係的方式。圖中的節點表示事務。從T1指向T2的邊,如T1->T2,表示T1的操作在T2前發生,並可能存在以下的依賴:

理解

WR依賴與WW依賴,均是T1提交後,T2才啟動,T2可以看到T1對數據的修改。

RW反依賴,是T1與T2並發時發生的。T2讀操作,T1寫操作,無論T2讀先執行還是T1寫先執行,依賴關係均是讀操作指向寫操作,因為讀操作讀到的記錄版本比寫操作老。

為什麼不考慮RW依賴?因為讀操作事務提交後,不會對之後啟動的寫操作事務產生影響。

2.2串列理論

定理1

在發生異常有向圖環中,必然存在三個事務T1到T2、T2到T3存在讀寫反向依賴,並且T3最先提交。

推論2

T1與T2並發,T2與T3並發。

採用上述方式描述write skew與只讀事務異常。

write skew依賴圖

只讀事務異常依賴圖

定理的理解

只讀事務異常案例中,依賴關係如定義所述,T1與T2有RW反向依賴、T2與T3有反向依賴,且T3先提交。這裡可以發現,T1與T2哪一個先提交,均會引起異常。

T1與T3可以為相同事務,write skew就是此種情況。

為什麼是T3提交而不是T2先提交呢?從只讀事務異常案例中,可以看出T1與T2先提交均不會引起異常。

推論的理解

推論中說T1與T2並發,T2與T3並發。並不是說T1、T2、T3同時並發。例如,T2與T3並發,T3提交之後,T1才啟動,此時只有T1與T2並發。

因此T3提交後,其與T2的反向依賴關係必須要保留到T1結束。

2.3 SSI大致處理流程

Cahill介紹了一種SSI檢查方法,該方法檢查運行中的事務依賴關係圖,並選擇事務abort,以保證事務的一致性。SSI方法類似於一種基於串列圖檢測的並發控制協議,該協議中跟蹤所有的依賴,並避免cycle的形成。SSI與其不同,SSI只檢測"dangerous structure",即有一條入邊和一條出邊的結構。當發現此結構時,SSI選擇事務回滾。根據定理1,SSI的方法可以保證串列化,但卻會有誤殺,當僅僅有dangerous structure而沒有cycle時。SSI的好處是,不需要記錄WR和WW依賴,同時dangerous structure的識別要比cycle檢測工作量小很多。

與S2PL和OCC相比,SSI並發性好。在並發事務中,出現一個RW反向依賴時,2PL和OCC都有阻塞,而SSI不會。

SSI需要檢測到多版本讀寫下的衝突的情況,對於讀操作需要加SIREAD鎖(讀寫不阻塞,只用於生成RW反向依賴。與S2PL不同,事務commit後SIREAD不能釋放,因為與該事務之前並發的事務,可能會寫該事務讀過(加SIREAD鎖)的記錄。

SSI的variants

Cahill介紹了一種優化的SSI,可以減少誤殺。原有的SSI只判斷dangerous structure,根據定理1,優化的SSI上增加了T3先提交的判斷。PG的SSI演算法採用了該方法。優化的SSI也存在誤殺,因為並沒有判斷依賴中的cycle。

PSSI記錄了所有的WR、WW依賴與RW反向依賴,可以判斷cycle,從而消除所有的誤殺。PG的SSI因為性能的原因,沒有採用此方法。

2.4 只讀事務的優化

大量的應用場景中,存在只讀事務比例高,執行時間長的特點。SSI讀操作加SIREAD鎖會帶來很大負擔。為對只讀事務進行優化,發展了只讀事務異常的串列化理論。基於該理論,做了兩點優化

  • 採用read-only snapshot ordering優化,減少了誤殺。
  • 識別safe snapshot。採用safe snapshot的只讀事務可以不採用SIREAD鎖。進一步發展出deferrable事務,該事務可以延遲執行以確保採用safe snapshot。

只讀事務串列理論 定理3

解釋:當dangerous structure結構被識別後,如果T1是只讀事務,除非T3在T1前提交,否則不會產生異常。對於只讀事務,是否產生異常,與其獲取snapshot的時間有關,與提交無關。

safe snapshot

基於定理3,可得到Safe snapshot。

T1、T2、T3,當前不存在事務T2到T3的RW反依賴,且T3在T1前已經提交,則T1獲取的就是safe snapshot。

safe snapshot不能T1啟動時就完全確定。例如,T1啟動時,T2到T3有RW反向依賴,但T1到T2是否有反向依賴,此時還不確定。PostgreSQL的實現中,T1啟動時,記錄活躍的事務,並採用SIREAD鎖。當所有活躍事務列表事務都結束,且沒有定理3的情況時,T1釋放其獲取的SIREAD,在其後的操作中不再申請SIREAD鎖。

deferrable事務

對於大數量的查詢,根據safe snapshot,T1獲取活躍事務列表後,可以等待列表中的所有事務結束,再啟動查詢,完全避免對SIREAD的獲取。

3.PG中SSI的實現與優化

在BerkeleyDB和InnoDB中已經有SSI的實現,PG中SSI的實現與其不同。由於沒有可以直接利用的鎖管理器,PG中新實現了一個鎖管理器,由於沒有可以直接利用的鎖管理器,在PG上實現SSI難度很大。

3.1基本實現

PG的基本並發控制結構

記錄與索引。記錄採用多版本。沒有原地修改,修改操作轉換為刪除與插入記錄,記錄的版本直接通過ctid連接(老版本指向新版本)。索引沒有版本信息,記錄的索引變化會新建索引項。非索引項的修改,採用hot技術。

可見性。記錄中包含xmin與xmax欄位,分別是插入與刪除的事務。事務是否已提交可以通過clog查詢。如果根據本事務的snapshot查詢,xmin或xmax已經完成且clog的信息為已經提交,則操作效果對本事務可見。

PG中實現了多個類型鎖。SpinLock為忙等Mutex,LWLock為讀寫鎖(有等待,無死鎖檢查,類似latch), RegularLock為事務級別鎖(有等待,有死鎖檢測),行鎖無衝突時,鎖信息保存在記錄中(通過xmin與xmax狀態表示),有衝突時保存在RegularLock中。

衝突檢測(檢測RW反依賴)

SSI lock manager

  • 鎖模式:只有一個SIREAD類型鎖
  • 鎖的粒度:表、頁、行,意圖鎖採用index page的粒度,未來計劃改進為next-key locking。支持鎖粒度的升級。
  • 未支持意圖鎖。在各個級別依次檢查。
  • 鎖持續的時間:如前面的分析,SIREAD要維持到與本事務一起活躍的事務結束時。
  • 額外的處理
    • 與DDL操作的關係(自己未分析過DDL鎖,未理解,待分析)
    • 數據重組時,鎖的對象的ID(記錄或index)發生變化,鎖的粒度升級為表。

讀寫處理

  • 讀到一個記錄時的處理:當並行事務對該記錄有寫鎖時(通過snapshot查詢,xmin或xmax未完成),產生RW反依賴。
  • 寫一個記錄時的處理:通過SSI lock manager查詢,檢查記錄是否有SIREAD鎖,範圍查詢使用index page的SIREAD鎖。當數據對象有SIREAD鎖時,產生RW反依賴。

衝突跟蹤(事務依賴關係的數據結構)

為每個事務記錄所有的RW反依賴。RW反依賴中指向了關聯的事務。

記錄詳細的信息有利於以下處理。

  • 實現commit ordering優化。見SSI的variants。
  • 只讀事務優化。
  • 發現dangerous structure時的abort處理
  • 內存優化(aggressive清理)

衝突解決

當dangerous structure被識別,且提交順序(定義1中,T3先提交)滿足時,選擇某事務回滾,以避免異常發生。

選擇回滾的事務滿足safe retry原則:該事務被重試時,不應因為原先的情況,再次失敗(不準確,大意)。

safe retry原則:

  • T3提交後,才考慮回滾事務。
  • T1與T2中選擇T2回滾,因為T2再次執行不會與已經提交的T3有RW關係,而T1再次執行會與T2再次建立RW關係。
  • T2與T3都提交的情況下,回滾T2,再次執行T2不會有RW關係。

3.2內存優化

面對問題

與傳統鎖管理器相比,SILOCK和RW反依賴不能在事務結束後釋放,會佔用更多的內存資源。

由於鎖表和事務依賴關係可佔用的內存大小是固定的(配置文件中指定),需要提供降級能力。在長時間運行事務的情況下,系統不應沒有內存資源而拒絕連接,應該通過提高鎖的粒度(這樣會提高誤判率),減少內存佔用,從而能接受新的連接。

PG採用進程間共享內存是使用System V的shared memory。默認配置大小為32M(這點自己不理解)。鎖表的內存大小無法動態分配。

解決方法

(1)safe snapshot和延遲事務降低了長時間運行的只讀事務的影響。

(2)細粒度到粗粒度的鎖對象升級。

(3)aggressive cleanup 已提交事務的狀態。

  • 對於提交事務,其鎖表與RW反依賴要保存多長時間?對於SILOCK,需要保存到與其並發的所有事務結束,而事務依賴的信息需要保留更長的時間。(文章中下面舉的例子感覺有問題,自己理解如下)。定理1中,例如,T2與T3並發,T3提交之後,T1才啟動,此時只有T1與T2並發。因此T3提交後,其提交順序號必須要保留到T1結束。為防止此問題,在事務節點中記錄與其衝突已事務的最早提交順序號。
  • 很明顯,當系統中的活躍事務只有隻讀事務時,已提交事務的SILOCK可以被釋放。
  • 已提交事務的RW反依賴中in的信息可以被丟棄。(自己理解in是寫信息,out是讀信息,應該是out信息可以丟棄)

(4)已提交事務狀態的summarization

PG的SSI中能夠保存的已提交事務數量是固定的。當有更多已提交事務時,需要對已提交事務的信息進行summarize。通常,只需要檢測到當前事務與之前已提交的事務發生了衝突,並不需要知道具體的已提交事務。概括在減少內存佔用的同時,會提高誤判率。

已提交事務狀態的概括是基於以下兩個發現:

  • 活躍事務寫一條記錄時,需要判斷是否某些已提交事務(之前並發的)讀過此記錄。此衝突只需要知道有已提交事務獲取了SILOCK,並不需要知道具體的事務。將已提交事務的SLock關聯到一個dummy lock,並在SILOCK中記錄最新事務的提交順序號,用於釋放SILOCK。
  • 活躍事務讀一條記錄時,需要判斷是否某些已提交事務(之前並發的)寫過此記錄。兩種可能的dangerous structure為:

對於第二種情況,需要知道T3的提交順序。在概括的情況下,由於此信息被丟棄,增加了一個事務號到與其衝突的最老已提交事務號,來解決此問題。

3.3對主要模塊的影響

兩階段提交

顯示兩階段提交時,事務會處於prepared狀態。對於非SI鎖,PG將該prepared事務獲取的鎖會保存在磁碟上,這樣故障恢復時,可以從磁碟中恢復該prepared事務的鎖信息。對於SSI鎖表及事務依賴關係,佔用的容量大,這麼做不可行。

由於Prepared成功的事務必須能提交成功,一個優化思路是在Prepared時,做一致性檢查。通過檢測的事務,需要做summarize。summarization信息是需要保存到磁碟的。

流複製

目前沒有在Slave實現SSI。我理解主要是因為查詢操作不寫日誌,為了在Slave構造鎖表,需要將SI Lock操作寫日誌,有工作量。更重要的是,由於safe snapshot機制,slave可以讀到一致的數據。擴展safe snapshot機制,Master生成Safesnapshot,並通過xlog複製到Slave節點。Slave節點的讀操作使用此Snapshot。

保存點與子事務

自己對於保存點和子事務,未做分析。待處理。

索引類型

目前謂詞鎖是基於B+樹,後面計劃擴展到GiST索引。

3.4 SILOCK加鎖案例分析

執行write skew案例,查看加鎖信息。

創建表、插入數據、查看索引名

create table a5b( a int primary key, b int) ;

insert into a5b values(1,100);

insert into a5b values(2,200);

查看數據文件

SELECT lp, lp_flags, t_xmin, t_xmax, t_ctid,

to_hex(t_infomask), explain_infomask(t_infomask)

FROM heap_page_items(get_raw_page(a5b, 0));

查看索引

SELECT * FROM bt_metap(a5b_pkey);

SELECT * FROM bt_page_items(a5b_pkey, 1);

查看錶與索引的文件oid

select relname, relfilenode from pg_class where relname like a5%;

查詢鎖表

select pid, transactionid, locktype, mode,relation, page, tuple, granted from pg_locks where pid !=pg_backend_pid() and locktype!=virtualxid order by pid, locktype;

可看出兩個會話分別在記錄和索引頁上獲取了SILOCK鎖。

4.參考資料

論文

《A Critique of ANSI SQL Isolation Levels》

《Serializable Snapshot Isolation in PostgreSQL》

《Designing Data-Intensive Applications》第7章

《Transaction Processing Concepts and Techniques》 第7、8章

-------------------------------------------------------------------------------

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證)


推薦閱讀:

Omid Transaction Processing
面試必備技能:JDK動態代理給Spring事務埋下的坑!

TAG:資料庫事務 | PostgreSQL | 一致性 |