在openGauss事務機制(上)的介紹中,已經瞭解當資料庫中存在併發執行事務的情況下,要保證ACID特性,需要一些特殊的機制來支援。併發控制就是這樣的一種控制機制,能夠保證併發事務同時訪問同一個物件或資料下的 ACID特性。
openGauss併發控制是十分高效的,其核心是 MVCC和快照機制。如openGauss事務機制(上)所述,透過使用 MVCC和快照,可以有效解決讀寫衝突,使得併發的讀事務和寫事務工作在同一條元組的不同版 本上,彼 此不會相互阻塞。對於併發的兩個寫事務, openGauss透過事務級別的鎖機制(事務執行過程中持鎖,事務提交時釋放鎖),來保證寫事務的一致性和隔離性。
另一方面,對於底層資料的訪問和修改,如物理頁面和元組,為了保證讀、寫操作的原子性,需要在每次的讀、寫操作期間加上共享鎖或排他鎖。當每次讀、寫操作完成之後,即可釋放上述鎖資源,無須等待事務提交,持鎖視窗相對較短。
(一)讀-讀併發控制
在絕大多數情況下,併發的讀-讀事務是不會也沒有必要相互阻塞的。由於沒有修改資料庫,因此每個讀事務使用自己的快照,就能保證查詢結果的一致性和隔離性;同時,對於表的底層的物理頁面和元組,如果只涉及讀操作,只需要對它們加共享鎖即可,不會發生鎖等待的情況。
一個比較特殊的情況是執行SELECT FOR UPDATE 查詢。該查詢會對所查到的每條記錄在元組層面加排他鎖,以防止在查詢完成之後,查詢結果集被後續其他寫事務修 改。該語句獲取到的元 組排他鎖,在事務提交時才會釋放。 對於併發的SELECT FOR UPDATE事務,如果它們的查詢結果集有交集,那麼在交集中的元組上會發生鎖衝突和鎖等待。
(二) 讀-寫併發控制
如
openGauss事務機制(上)圖10的例子所示,openGauss中對於讀、寫事務的併發控制是基於MVCC和快照機制的,彼此之間不會存在事務級的長時間阻塞。相比之下,採用兩階段鎖協議(Two-Phase Locking Protocol,2PL 協議)的併發控制(如IBM DB2資料庫),由於讀、寫均在記錄的同一個版本上操作,因此排他鎖等待佇列後面的事務至少要阻塞到持鎖者事務提交之後才能繼續執行。
另一方面,為了保證底層物理頁面和元組的讀、寫的原子性,在實際操作頁面和元組時,需要暫時加上相應物件的共享鎖或排他鎖,在完成物件的讀、寫操作之後,就可以放鎖。對於所有可能的三種讀-寫併發場景,即查詢-插入併發、查詢-刪除併發和查詢-更新併發,在圖1、圖2和圖3中分別給出了它們的併發控制示意圖。
圖1 查詢-插入併發控制示意圖
圖2 查詢-刪除併發控制示意圖
圖3 查詢-更新併發控制示意圖
(三) 寫-寫併發控制
雖然透過 MVCC,可以讓併發的讀-寫事務工作在同一條記錄的不同版本上(讀老版本,寫新版本),從而互不阻塞,但是對於併發的寫-寫事務,它們都必須工作在最新版本的元組上,因此如果併發的寫-寫事務涉及同一條記錄的寫操作,那麼必然導致事務級的阻塞。
寫-寫併發的場景有以下6種:插入-插入併發、插入-刪除併發、插入-更新併發、刪除-刪除併發、刪除-更新併發、更新-更新併發。下面就插入-插入併發、刪除-刪除併發和更新-更新併發的控制流程做簡要描述,另外三種併發場景下的控制流程讀者可自行思考。
圖4為插入-插入事務的併發控制流程圖。每個插入事務都會在表的物理頁面中插入一條新元組,因此並不會在同一條元組上發生併發寫衝突。然而,當表具有唯一索引時,為了避免違反唯一性約束,若併發插入-插入事務在唯一鍵上有衝突(即鍵值重複),後來的插入事務必須等待先來的插入事務提交以後,再根據先來插入事務的提交結果,才能進一步判斷是否能夠繼續執行插入操作。如果先來插入事務提交了,那麼後來插入事務必須回滾,以防止唯一鍵重複;如果先來插入事務回滾了,那麼後來插入事務可以繼續插入該鍵值的記錄。
圖4 插入-插入併發控制示意圖
圖5為刪除-刪除事務的併發控制流程圖。對於併發的刪除-刪除事務,它們都會嘗試去修改同一條元組的xmax值。一般透過頁面排他鎖來控制該衝突。對於後加上鎖的刪除事務,它在再次標記元組xmax值之前,首先需要判斷先來刪除事務(即元組當前xmax事務號對應的事務)的提交結果。如果先來刪除事務提交了,那麼該元組對後來刪除事務不可見,後來刪除事務無元組需要刪除;如果先來刪除事務回滾了,那麼該元組對後來刪除事務依然可見,後來刪除事務可以繼續執行對該元組的刪除操作。
圖5 刪除-刪除併發控制示意圖
圖6為更新-更新事務的併發控制流程圖。併發的更新-更新事務與併發刪除-刪除事務類似,它們首先都會嘗試去修改同一條元組的xmax值。一般透過頁面排他鎖來控制該衝突。對於後加上鎖的更新事務,它在再次標記元組 xmax值之前,首先需要判斷先來更新事務(即元組當前 xmax事務號對應的事務)的提交結果。如果先來更新事務提交了,那麼該元組對後來更新事務不可見,此時,後來更新事務會去判斷該元組更新後的值(先來更新事務插入)是否還符合後來更新事務的謂詞條件(即刪除範圍),如果符合,那麼後來的更新事務會在這條新的元組上進行更新操作,如果不符合,那麼後來的更新事務無元組需要更新;如果先來更新事務回滾了,那麼該元組對後來更新事務依然可見,後來更新事務可以繼續在該元組上進行更新操作。
圖6 更新-更新併發控制示意圖
(四)併發控制和隔離級別
在上文介紹寫-寫併發控制的機制時,其實預設了使用讀已提交的隔離級別。
回顧圖4、圖5和圖6,可以發現,當在某條元組上發生併發寫-寫衝突時,原本先來事務是在後來事務的快照中的,後來事務是不應該看到先來事務的提交結果的,但是為了解決上述衝突,後來事務會等待先來事務提交之後,再去校驗先來事務對元組的操作結果。這種方式是符合讀已提交隔離級別要求的,但是顯然後來事務在等待之後,又重新整理了自己的快照內容(將先來事務從快照中移除)。
基於上述原因,在 MVCC和快照隔離的併發控制策略下,若使用可重複讀的隔離級別,當發生上述寫-寫衝突時,後來事務不會再等待先來事務的提交結果,而是將直接報錯回滾。這也是openGauss在可重複讀隔離級別下,對於寫-寫衝突的處理模式。
進一步來說,如果要支援可序列化的隔離級別,對於使用 MVCC和快照隔離的併發控制策略,需要解決寫偏序(Write Skew)的異常現象,有興趣的讀者可以參考2008 年SIGMOD最佳論文SerializableI solationf Or Snapshot Databases。
(五) 物件屬性的併發控制
在上面併發控制的介紹中,覆蓋了 DML和查詢事務的併發控制機制。對於 DDL語句,其雖然不涉及表資料元組的修改,但是其會修改表的結構(Schema),因此很多場景下不能和 DML、查詢併發執行。
以增加欄位的 DDL事務和插入事務併發執行為例,它們的併發執行流程如圖7 所示。首先,DDL事務會獲取表級的排他鎖,而 DML事務在執行之前,需要獲取表級的共享鎖。DDL事務持鎖之後,會執行新增欄位操作。然後,DDL事務會給其他所有併發事務傳送表結構失效訊息,告訴其他併發事務,這個表的結構被修改了。最後,DDL事務釋放表級排他鎖,提交返回。
圖7 DDL-DML併發控制示意圖
DDL事務放鎖之後,DML事務可以獲取到該表的共享鎖。加鎖之後,DML 事務首先需要處理所有在等鎖過程中可能收到的表結構失效訊息,並載入新的表結構資訊。然後,DML才可以執行增刪改操作,並提交返回。
(六) 表級鎖、輕量鎖和死鎖檢測
上文已經向讀者初步介紹了在事務併發控制中,需要有鎖機制的參與。圖7 DDL-DML併發控制示意圖實上,在openGauss中,主要有兩種型別的鎖:表級鎖和輕量鎖。
表級鎖主要用於提供各種型別語句對於表的上層訪問控制。根據訪問控制的排他性級別,表級鎖分為1級到8級鎖。對於兩個表級鎖(同一張表)的持有者,如果他們持有的表級鎖的級別之和大於等於8級,那麼這兩個持有者的表級鎖會相互阻塞。
在典型的資料庫操作中,查詢語句需要獲取1級鎖,DML 語句需要獲取3級鎖,因此這兩個操作在表級層面不會相互阻塞(這得益於10.3.2節中介紹 MVCC和快照機制)。相比之下,DDL語句通常需要獲取8級鎖,因此對同一張表的 DDL操作會和查詢語句、DML語句相互阻塞。正如圖7的例子所示,以修改表結構型別的 DDL語句為代表,如果允許在該 DDL 執行過程中同時插入多條資料,那麼前後插入的資料的欄位個數可能不一致,甚至相同欄位的型別亦可能出現不一致。
另一方面,在建立一個表的索引過程中,一般不允許有併發的 DML 操作,否則可能會導致索引不正確,或者需要引入複雜的併發索引修正機制。在openGauss中,建立索引語句需要對目標表獲取5級鎖,該鎖級別和 DML的3級鎖會相互阻塞。
在openGauss中,為表級鎖的所有等待者維護了等待佇列資訊。基於該等待佇列,openGauss對於表級鎖提供了死鎖檢測。死鎖檢測的基本原理是嘗試在所有表級鎖的等待佇列中尋找是否存在能夠構成環形等待佇列的情況,如果存在環形等待佇列,那麼就表示可能發生了死鎖,需要讓其中某個等待者回滾事務退出佇列,從而打破該環形等待佇列。
在openGauss中,第二種廣泛使用的鎖是輕量鎖。輕量鎖只有共享和排他兩種級別,並且沒有等待佇列和死鎖檢測。一般輕量鎖並不對資料庫使用者提供,僅供資料庫開發人員使用,需要開發人員自己來保證併發情況下不會發生死鎖的場景。在本文中曾經介紹過的頁面鎖即是一種輕量鎖,表級鎖也是基於輕量鎖來實現的。
openGauss分散式事務
前面簡要介紹了單機事務和分散式事務的區別,也指出了在分散式情況下, 可能存在特有的原子性和一致性問題。本節主要介紹在openGauss中,如何保證分散式事務的原子性和強一致性。
(一)分散式事務的原子性和兩階段提交協議
為了保證分散式事務的原子性,防止出現
openGauss事務機制(上)中圖2所示的部分 DN 提交、部分DN 回滾的“中間態”事務,openGauss採用兩階段提交(2PC)協議。
圖8 兩階段提交流程示意圖
如圖8所示,兩階段提交協議將事務的提交操作分為兩個階段:
- 準備階段(prepare phase),在這個階段,將所有提交操作所需要用到的資訊和資源全部寫入磁碟,完成持久化;
- 提交階段(commitphase),根據之前準備好的提交資訊和資源,執行提交或回滾操作。
兩階段提交協議之所以能夠保證分散式事務原子性的關鍵在於:一旦準備階段執行成功,那麼提交需要的所有資訊都完成持久化下盤(寫入磁碟),即使後續提交階段圖8 兩階段提交流程示意圖。某個 DN 發生執行錯誤,該 DN 可以再次從持久化的提交資訊中嘗試提交,直至提交成功。最終該分散式事務在所有 DN 上的狀態一定是相同的,要麼所有 DN 都提交, 要麼所有 DN 都回滾。因此,對外來說,該事務的狀態變化是原子性的。
表1總結了在openGauss分散式事務中的不同階段,如果發生故障或執行失敗,分散式事務的最終提交/回滾狀態,讀者可自行推演,本文不再贅述。
表1 發生故障或執行失敗時事務的最終狀態
故障或執行失敗階段 |
事務最終狀態 |
SQL語句執行階段 |
回滾 |
準備階段 |
回滾 |
準備階段和提交階段之間 |
可回滾,亦可提交 |
提交階段 |
提交 |
(二)分散式事務一致性和全域性事務管理
為了防止
openGauss事務機制(上)中圖3的瞬時不一致現象,支援分散式事務的強一致性,一般需要全域性範圍內的事務號和快照,以保證全域性 MVCC 和快照的一致性。在 openGauss中, GTM 負責提供和分發全域性的事務號和快照。任何一個讀事務都需要到 GTM 上獲取全域性快照;任何一個寫事務都需要到 GTM 上獲取全域性事務號。
在
openGauss事務機制(上)中圖3加入 GTM,並考慮兩階段提交流程之後,分散式讀-寫併發事務的流程如圖9所示。對於讀事務來說,由於寫事務在其從 GTM 獲取的快照中,因此即使寫事務在不同 DN 上的提交順序和讀事務的執行順序不同,也不會造成不一致的可見性判斷和不一致的讀取結果。
圖9 讀-寫併發下全域性事務號和快照的分發流程示意圖
細心的讀者會發現,在圖9的兩階段提交流程中,寫事務 T1在各個 DN 上完成準備之後,首先第一步是到 GTM 上結束 T1事務(將 T1從全域性快照中移除),然後第二步到各個 DN 上進行提交。在這種情況下,如果查詢事務 T2是在第一步和第二步之間在 GTM 上獲取快照,併到各個 DN 上執行查詢,那麼 T2事務讀到的 T1事務插入的記錄v1和v2,它們xmin對應的 XID1已經不在 T2事務獲取到的全域性快照中,因此v1和v2的可見性判斷會完全基於 T1事務的提交狀態。然而,此時XID1對應的T1事務在各個 DN 上可能還沒有全部或部分完成提交,那麼就會出現各個 DN 上可見性不一致的情況。
為了防止上面這種問題出現,在openGauss中採用本地二階段事務補償機制。如圖10所示,對於在 DN 上讀取到的記錄,如果其xmin或者xmax已經不在快照中,但是它們對應的寫事務還在準備階段,那麼查詢事務將會等到這些寫事務在 DN 本地 完成提交之後,再進行可見性判斷。考慮到透過兩階段提交協議,可以保證各個DN上事務最終的提交或回滾狀態一定是一致的,因此在這種情況下各個 DN 上記錄的可見性判斷也一定是一致的。
圖10 讀-寫併發下本地兩階段事務補償流程示意圖
小結
本文主要結合openGauss的事務機制和實現原理,基於顯式事務和隱式事務,介紹事務塊狀態機的變化,以及openGauss事務 ACID特性的實現方式。尤其對於分散式場景下的事務原子性和一致性問題,介紹openGauss採取的多種解決技術方案,以保證資料庫最終對外呈現的 ACID不受分散式執行框架的影響。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69997967/viewspace-2922538/,如需轉載,請註明出處,否則將追究法律責任。