Weak Isolation Levels – Read Committed and Snapshot Isolation

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

前言

如果兩個 transaction 沒有接觸到相同的資料,則它們可以很愉快的 並發 (concurrent) 執行,因為他們彼此不依賴。

並發的問題只會發生在當某個 transaction 要讀取的資料正在被另一個並發執行的 transaction 修改,或者兩個 transaction 同時修改一樣的資料。

並發的 bug 很難測試或重現,所以資料庫才會透過 ACID,向我們保證不會發生並發的 bug,就理論上來說,隔離性應該能保護工程師遠離並發的惡夢,所以最好就是不要用並發啦,也就是實施 序列化隔離 (serializable isolation), 一次只有一個 transaction 在執行完全符合隔離性保證。

一次只能讓一個 transaction 執行,這就代表序列化隔離會有很嚴重的效能問題,因此在現實世界中,許多支援 ACID 的資料庫,大多數的預設都是使用 弱等級的隔離 (Weak Isolation Levels),因為隔離性等級較弱,所以會比較難理解,也代表還是有可能會出現並發資料問題或並發 bug,所以就讓我們來好好了解各個弱等級隔離的差異,然後搭配應用程式開發達成盡量減少並發 bug 吧!

Read Committed

等級 1 的隔離是 read committed ,它保證了以下兩點:

  1. 讀取資料時,你只會讀取到已經被 commit 的資料(無 dirty read)。
  2. 寫入資料時,你只會寫入至己經被 commit 的資料(無 dirty write)。

讓我們研究研究這 2 點。

No Dirty Read

dirty read 就是在你的 transaction 裡讀到另一個 transaction 還沒 commit 的資料。

無 dirty read 的結果如下圖,User 2 只會讀取到已被 commit 的資料(等於是 User 1 的多次寫入在 commit 後會一起成為可視)。

No Dirty Write

dirty write 就是一個 transaction 在還沒 commit 的情況下,其值被另一個 transaction 給覆寫了。

read committed 等級的隔離該如何避免 dirty write 呢?一般的做法就是延遲第二個 transaction 的寫入,直到第一個的 transaction 寫入 commit 或中斷 (aborted)。

如果 transaction 是需要更新多個物件,如下圖,dirty write 就會導致汽車是 Bob 買到,但發票是寄給 Alice 了。

然而,read committed 無法避免如下圖針對 計數器 (counter) 資源的 競爭條件 (race condition) 寫入 , User 2 的寫入是發生在 User 1 已經 commit 之後的動作,所以符合 read committed 的保證,無 dirty write。但它的值依舊不正確,我們會在 Day 5 討論如何安全的在 counter 累加 1。

實現 Read Committed

read committed 是最流行、常用的隔離等級,也是許多資料庫的預設隔離等級。

避免 dirty write 的實現可以使用 row-level 的鎖,當 transaction 想要修改特定物件 (row 或 document) 時,它必須取得該物件的鎖,持有該鎖直到該 transaction commit 或中斷 (aborted),一次只有一個 transaction 能取得物件鎖。

而避免 dirty read 的實現呢?一個選項是採用避免 dirty write 的方法,有物件鎖才能讀取資料;但倘若此時有個跑很久的寫入 transaction 正在寫入資料呢?所有的讀取就得排隊等 transaction 結束才能作業了,如此就會嚴重傷害那些 read-only transaction 的回覆時間了,顯然這不是個好方法。

基於以上理由,大多數的資料庫實現 避免 dirty read 的方法就是像上圖 7-4 那樣,在 User 1 執行資料寫入時,除了要取得物件鎖之外,資料庫也會先記住舊的資料讓其他的 transaction 讀取,commit 後就替換成新的資料。

Snapshot Isolation 和 Repeatable read

先來看個 read committed 等級的隔離下會發生的靈異現象吧!如下圖的 Alice 在銀行各存了 500 元在兩個帳戶中,總額 1000 元,爾後有個 transaction 執行 100 元的轉帳作業,如果 Alice 在轉帳前查了 Account 1,而在轉帳結束後才去查了 Account 2, 會發現兩個帳戶的總金額為 900 元,100 元消失在空氣中了,比被通膨吃掉還慘。

這種異常情形稱 nonrepatable readread skew,儘管 Alice 在查一次就正常了,但有些情況則不允許這種不一致發生:

  • 備份 (Backup) 備份作業通常需要整個資料庫的複製,如果在備份期間發生 read skew,若不幸要用該備份復原資料的話,Alice 的 100 元就真的消失了。
  • 分析需求和資料檢查 有時分析需求需要查大量的資料,或者資料需要定期做檢查(監測腐壞資料),這 2 種情形都會造成無意義且會浪費人力資料的結果。

快照隔離 (Snapshot Isolation) 一般是解決 read skew 問題的方式,其概念就是每一個 transaction 開始時,都是從一致 (consitent) 的快照中讀取資料,期間即使其他 transaction 已經修改資料了(已 commit),該 transaction 還是一樣會讀舊的資料,可以想像成資料就被凍結在 transaction 開始的時間點,它尤其適合用在長時間的 transaction 上。

實現快照隔離 (Snapshot Isolation)

就像實現 read committed 那般,避免 dirty write 的方式也是使用物件鎖,避免 dirty read 則不能使用任何鎖,我們首先要確保 寫入跟讀取不會互相影響,所以為了實現快照隔離,資料庫需要一個機制去保存不同版本的物件資料,因為多個 transaction 可能會在同個時間點看到多個快照的物件資料,該機制稱為 MVCC 多版本並發控制 (multi-version concurrent control)

如果資料庫支援快照隔離,則前一小節講的 read committed 隔離就能一起使用 MVCC 實作了,差別只是 read committed 是在同個 transaction 下的每一次查詢都拿不同的快照,而快照隔離都是拿同一個快照而已。

下圖說明了以 MVCC 為基礎的快照隔離機制如何運作,當一個 transaction 開始時,資料庫會給定唯一的 txid (transaction ID),每筆資料會多 2 個欄位,created_bydeleted_by,當資料寫入時,created_by 會記錄寫入的 txid ,而 deleted_by 初始是空值,當資料被刪除時,資料不會真的被刪掉,而是在 deleted_by 標記是哪個 txid 刪除,往後垃圾收集器會定期去清除無 transaction 在用且 deleted_by 有值的快照資料。

下圖的 txid 13 在更新 Account 1 的金額為 600 時,就會看到 500 那筆被標記被 tx id 13 刪除,然後同時建立一筆 created_by = 13 的資料,然後 txid 13 在更新 Account 2 的資料時亦同;txid 12 查詢時,因為 txid 13 是比較晚開始的 transaction,所以任何 txid 13 的寫入都會被忽略,所以 txid 12 只會查到符合一致性原則的快照資料。

一致性快照的可視化規則

簡單來說,一個物件要可視必須符合以下 2 個條件:

  1. 當讀取者的 transaction 開始時,物件已被建立且已被其他 transaction commit。
  2. 該物件未被標記成已刪除(圖 7-7 的例子為 deleted_by 有值),或者當讀取者的 transaction 開始時,物件已被標記刪但還沒 commit。

所以啦,若有個超長時間的 transaction 在執行,只要多付出一點點的 overhead,該 transaction 不用擔心 read skew 的問題。

作者的抱怨:快照隔離在 Oracle 稱為 Serializable ,而在 PostgreSQL 和 MySQL 稱為 repeatable read ,他認為 SQL 標準裡的隔離等級有瑕疵、曖昧不清且不精準的,所以一個名詞可以有很多不同的意思。

tshine73
tshine73
文章: 50

發佈留言

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