Serializability Isolation

本文為 Design Data Intensive Applications 的書摘 + 個人心得。

昨天談到 write skew 和 phantoms ,是 2 種特別難重現的 競爭條件 (race condition) 情況,也就代表無法針對這些情況做測試,這些只有在你衰小要找奇怪的 bug 時才會遇到,這是個老問題了,而這一切的解法也就顯而易見,使用 序列化隔離 (serializable isolation)

序列化隔離是最強的隔離等級,儘管 transaction 能並發執行,但它保證了最終的執行結果等同一次只執行一個 transaction,重點是 連續 (serially) 這個字,表示無並發性 (without concurrency),所以它就能避免所有的競爭條件。

序列化隔離的實現方法有 3 種:

​ (1) 字面上的,連續 (serially) 執行 transactions。

​ (2) 二階段鎖 (Two-Phase Locking 2PL),主宰數十年的實作方式。

​ (3) 優化並發控制技術,就像有了序列化能力的快照隔離。

(1) 連續執行 (Serial Execution) transaction

最簡單暴力直覺的實作方法,每一個時間點只會有一個 transaction 執行在單一執行緒上。

為什麼這方法變的可行了呢?歸功於越來越便宜和越來越強的 RAM,讓一切在 RAM 上執行變的可行;再加上 OLTP transaction 都是短小精幹的讀取跟寫入,長時間寫的讀取要用 OLAP + 快照隔離來區分。

但是!這就代表了這個實作方法的吞吐量很吃單一 CPU 核心效能,為了讓單一執行緒用的更有效率,書中建議我們可以將數個 transaction 會做的事合併成一個 預存程序 (stored procedures) 來執行,就是要減少網路 IO,盡量避免 交互式多語句 (interactive multi-statement) transaction,

以下延用 Concurrent Write 圖 7-8 的醫生排班案例。

所以 連續執行 (serial execution) 要可行,最好還是不要違反以下幾點的限制:

  • 每個 transaction 必須短小精幹,因為只要有一個慢的 transaction 就會拖延其他 transaction 處理。
  • 資料集必須能符合 RAM 的大小。
  • 寫入吞吐量必須小於單一 CPU 核心能處理的數量。

(2) 二階段鎖-2PL (Two-Phase Locking)

主宰了序列化隔離 30 幾年的實作算法,其實我們在 Weak Isolation Levels – Read Committed – No Dirty Write 小節 看到資料庫是怎麼用鎖去避免 Drity Write,二階段鎖 (Two-Phase Locking) 也是類似的概念,但鎖更強大,當沒有 transaction 正在寫入時,多個 transaction 允許並發讀取同一個物件,一旦有 transaction 想寫入物件,它會:

  • 如果 transaction A 想讀取物件,而 transaction B 想寫入物件,B 必須等待 A commmit 或中斷 (abort)。
  • 如果 transaction A 想寫入物件,而 transaction B 想讀取物件,B 也是必須等待 A commit 或中斷 (abort)。

在二階段鎖中,寫入只會阻檔讀取,反之亦然,這跟 快照隔離 的寫入跟讀取不會互相影響 的核心精神很不同,所以二階段鎖能避免所有的競爭條件寫入、Concurrent Write 的更新遺失 (lost update) 和 write skew。

實作二階段鎖 (Two-Phase Locking)

為了實現這個寫入跟讀取互相阻檔的資料庫 全物件鎖,鎖的狀態可以是 共享模式 (shared mode) 或者是 互斥模式 (exclusive mode),該鎖跟隨以下規則:

  • 當一個 transaction 想要讀取物件,它必須先獲取 共享模式 鎖,多個 transaction 被允許同時擁有 共享模式 鎖,但是一旦有另一個 transaction 有 互斥模式 鎖,其他 transaction 必須等待。
  • 當一個 transaction 想要寫入物件,它必須獲取 互斥模式 鎖,該鎖一次只能被一個 transaction 所擁有。
  • 當一個 transaction 是先讀取資料然後在寫入,它的鎖必須從 共享模式 升級成 互斥模式 ,這跟你直接獲取 互斥模式 鎖的意思一樣。
  • 當一個 transactino 獲取鎖之後,它必須持有該鎖直到該 transaction commit 或 abort,這也是二階段鎖的由來,獲取鎖,然後釋放。

用這麼多鎖免不了可能會發生 死結 (deadlock),也就是 2 個 transaction 彼此等待互相釋放鎖,幸運的是現在的資料庫會自動偵測死結,然後 abort,之後靠應用程式做重試。

二階段鎖 (Two-Phase Locking) 的效能

二階段鎖最大的缺點就是效能啦,transaction 的吞吐量和回應時間比起 弱等級隔離 (Weak Isolation Levels) 要糟上許多,其最大的原因就是要等待 互斥模式 鎖的釋放,如果你的 transcation 又執行的稍稍久一點,回應時間就爛掉了,所以 2PL 會有非常不穩定的延遲 (lantency),會有非常慢或非常快的回應時間百分位發生,還有剛剛講的死結問題也需要時間解決。

3. 序列化快照隔離 SSI (Serializable Snapshot Isolation)

難道 序列化隔離 (serializable isolation) 跟良好效能是矛盾的嗎?

在這裡跟您介紹 序列化快照隔離 (Serializable Snapshot Isolation) ,一種比快照隔離多了一點開銷的算法,但它提供了完整的序列化功能,它來自於 2009 年 Michael Cahill’s 的博士論文

悲觀 (Pessimistic) 和樂觀 (Optimistic) 並發控制

二階段鎖 (Two-Phase Locking) 看上去可以被稱為悲觀的並發控制機制,悲觀的大原則是說凡是有可能錯的就一定會出錯,也就是像 2PL 那樣要取得 互斥模式 (exclusive mode) 鎖才能寫入資料,其他人最好還是乖乖等我!

連續執行 (Serial Execution) 更是悲到極致了,它本質上等同於每個 transaction 都得獲取整個資料庫的互斥鎖才能做事情。

相比之下序列化快照隔離就是屬於樂觀的並發控制機制,樂觀代表它不假設潛在的危害會發生,它希望一切都會好轉;當一個 transaction 想要 commit 時,資料庫才會檢查是否有不好的事情在發生(例如隔離無效),如果有,則該 transaction 會中斷且重試。

就像命名那樣,序列化快照隔離的基礎是快照隔離,所有的讀取都是來自有一致性的資料庫快照中 (Weak Isolation Levels – Snapshot Isolation),而最大的不同是序列化快照隔離多新增了一個演算法,在寫入期間去檢測是否發生連續的衝突然後決定哪個 transaction 要被中斷。

基於過時前提的決策

如同我們在 Concurrent Write 提過的,快照隔離可能會發生 write skew ,其模式就是 查詢-判斷-寫入,在 commit 時其原始查詢結果可能 不再是最新的,因為該資料可能同時被其他 transaction 修改。

換一種方式來說,這些 transaction 是基於一種 前提 來執行,以 Concurrent Write – 醫生 oncall 案例 來看,前提就是:目前正有 2 個醫生在 oncall,爾後在 commit 時,這個前提可能就不是為真了。

但資料庫怎麼知道應用程式對查詢結果的判斷邏輯為何呢?為了安全起見,資料庫會假設所有的查詢結果都可能都會被改變(查詢結果 = 前提),意味者該 transaction 的寫入可能會無效。

資料庫必須檢測 transaction 的前提是否過時,所以這裡有 2 個狀況需要考慮:

  • 檢測從腐敗 MVCC 物件版本讀取的資料(未 commit 的寫入發生在讀取之前)。
  • 檢測讀取期間的寫入(寫入發生在讀取之後)。

檢查腐敗的 MVCC 讀取

回憶一下 MVCC 的機制,當 transaction 從一致的快照讀取資料時,它會忽略其他未 commit 的 transaction 寫入,如下圖 7-10 (使用 Concurrent Write – 醫生 oncall 案例) ,transaction 43 讀取到的資料是 Alice on_call=true,因為 transaction 42 還沒 commit,然而,當 transaction 43 想要 commit 時,transaction 42 已經 commit 了,這意味者 transaction 43 commit 會被忽略,transaction 管理者會注意到快照資料已被影響,且 前提 的值不再是最新的了。

檢測讀取期間的寫入

現在來看第 2 個情況,如下圖 7-11,transaction 42 和 43 都查詢了 shift_id=1234 的班,如果這裡有個 shift_id 的 index,資料庫就會借用 index 進入點 1234 來記錄 transaction 42 和 43 正在讀取資料(如果沒有 index 就會追蹤在 table 等級中),其資料會保留到所有並發 transaction 都結束後才會清除。

當 transaction 寫入到資料庫時 ,它必須看一下 index 資料內是否也有正在讀取的資料被影響,transaction 42 和 43 會彼此留意資料有可能不再是最新的,所以 transaction 43 想要 commit 時,transaction 42 已 commit 所以發生衝突,故 transaction 43 被中斷了。

序列化快照隔離 (Serializable Snapshot Isolation) 的效能

樂觀並發控制在有非常高度競爭時(多個 transaction 嘗試存取相同物件)會表現的比較不好,代表了會有很多的片段 transaction 會需要被中斷。

但是跟 二階段鎖 (Two-Phase Locking) 比起來,序列化快照隔離有個最大的優勢就是 transaction 的寫入不用阻檔等待從其他 transaction 取得鎖,就像快照隔離那樣,寫入跟讀取不會互相影響,這個設計原則讓查詢延遲 (lantency) 變的可預測且變化較小,read-only 類型的查詢可以從一致的快照讀取資料而不需要取得任何鎖,適合用在讀取重的場景上。

而跟 連續執行 (Serial Execution) 比較呢?序列化快照隔離不必被限制上單一 CPU core 的吞吐量多少了。

影響序列化快照隔離效能最大的就是中斷的比率,所以它當然也希望所有 transaction 都是短小精幹(長時間只讀不寫的 transaction 也許 Ok);總而言之,它對緩慢 transaction 的敏感度遠遠小於 二階段鎖 跟 連續執行。

tshine73
tshine73
文章: 50

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *