複習資料庫的 Isolation Level 與圖解五個常見的 Race Conditions

Chester Chu

Locks

Exclusive Lock (Write Lock, X-Lock)

Transaction 在拿到資料的 Exclusive Lock 後就可以對資料做寫入的動作,這個時候其他 Transaction 則不能讀取,也不能寫入資料。

如果一筆資料被加上了 Shared Lock 或是 Exclusive Lock,就無法再拿到這筆資料的 Exclusive Lock,如果想要對這筆資料做寫入,就必須排隊等待,一直到資料上所有的 Shared Locks 或 Exclusive Lock 都被釋出後,再重新搶 Exclusive Lock。

Shared Lock (Read Lock, S-Lock)

資料被加上 Shared Lock 時,就無法再被加上 Exclusive Lock,所以其他 Transaction 將無法對這筆資料做寫入,但是還是可以繼續讀取這筆資料。概念有點像是不准你寫入,但是為了效能,我繼續與你 Share 資料讓你讀取。

比較特別的是,同一筆資料可以『同時』被多個 Transactions 加上 Shared Locks,當一比資料上有一個或多個 Share Locks 時,就沒有任何 Transaction 可以拿到 Exclusive Lock,也就沒有人可以對資料做寫入,必須等到所有 Shared Locks 都被釋出後再搶 Exclusive Lock。

但是當資料上只有 1 個 Shared Lock,而且這個 Shared Lock 的擁有者是 Transaction 自己本身時,Transaction 就可以直接將這個 Shared Lock 『升級』,變成 Exclusive Lock,直接對資料做寫入。

Range Lock

對一個 range 內的資料做 Lock,主要用來避免 Phantom 現象。例如如果執行下面的 MySQL 指令:

SELECT * FROM student WHERE height >= 170 FOR UPDATE;

除了被讀取到的資料會被加上 Exclusive Lock 外,在 height 從 170 到無限大的 range 也會被加上 Exclusive Lock,任何 height 介於這個 Range Lock 範圍內的資料都不能讀取也不能寫入。

以 InnoDB 來說,如果 Isolation Level 設定為 Serializable 的時候,其他 Transaction 不能讀取也不能寫入 height 介於 Range Lock 內的資料。但是如果 Isolation Level 設定為 Repeatable Read 的時候,因為 InnoDB 在 RR Isolation 的實作是採用 Snapshot Isolation,讀取時都是讀取 Snapshot 內的資料,所以讀取 Range Lock 範圍內的資料是可以成功的 (讀 Snapshot 內的資料),但是仍然不能寫入任何 height 介於 Range Lock 範圍內的資料。

Phenomena (Race Conditions)

Dirty Read

下圖中,Transaction B 讀到 Transaction A 才剛剛更新,但是還沒有 Commit 的資料 (quantity = 6)。之後 Transaction A 可能會再次更新資料,或是像圖中一樣進行 Rollback 所做的更動,Transaction B 就因此拿到髒掉的資料,這就是 Dirty Read。Read Committed 或是更高的 Isolation 可以避免這個現象。

Non-repeatable Read (Read Skew)

如果將資料庫 Isolation Level 設定為 Read Committed,就會像下圖中Transaction B 第一次讀取時只能拿到已經被 Commit 的資料 (quantity = 10),看不到 Transaction A 才剛剛更新,但是還沒 Commit 的資料 (quantity = 6)。但是當 Transaction B 第二次讀取時,拿到的就是 Transaction A Commit 之後的資料。在同一個 Transaction 中,重複讀取時會拿到不一致的資料就叫做 Non-repeatable Read。Repeatable Read 或是更高的 Isolation 可以避免這個現象。

Lost Update

下圖中,兩個 Transaction 同時進行賣出 Item A 的操作,Transaction B 理論上應該將 Transaction A 的更動納入考量,但是實際上卻是直接把 Transaction A 的更新覆蓋掉了,造成 Transaction A 的更新遺失,這就是 Lost Update。Serializable Isolation 可以避免這個現象,Repeatable Read Isolation 依照每個資料庫實作的不同而有不同的行為,有的無法避免 Lost Update (ex: MySQL InnoDB),有的則可以 (ex: PostgreSQL)。

Phantom

下圖中,Transaction A 第一次讀取所有 quantity 小於 5 的資料時,總共拿到 A、B 和 C 三筆,這時候 Transaction B 中途新增了一筆 D,當 Transaction A 第二次讀取時,變成總共拿到四筆資料,比原先多讀到資料 D,出現 Phantom 現象。Serializable Isolation 可以避免這個現象。Repeatable Read Isolation 依照每個資料庫實作的不同而有不同的行為。例如 PostgreSQL 可以完全避免 Phantom,但是 MySQL InnoDB 只能避免 Phantom Read,UPDATE 和 DELETE 等 DML 的寫入的操作則無法避免 Phantom 現象,詳細可以看:『對於 MySQL Repeatable Read Isolation 常見的三個誤解』

Write Skew

下圖中,兩個 Transaction 分別同時賣出 2 個 Item A,為了確保有庫存,在賣出前會先讀取現在庫存的數字,確認數字大於 2,扣掉賣出的數量後不會小於 0 ,才更新庫存。從下圖可以看到,兩個 Transaction 都滿足大於 2 這個『前提』,所以繼續進行更新庫存的操作,但是最後卻導致庫存數量等於 -1,違反庫存不能小於 0 的前提,這就是 Write Skew。

會造成 Write Skew 現象的情境通常會有下列 Pattern:

  1. 讀取資料,確認符合『前提』
  2. 更新寫入資料
  3. 寫入的資料會影響到其他 Transaction 對相同『前提』的判斷結果,如上圖兩個 Transaction 更新庫存的數量,讓庫存大於 2 的這個『前提』改變。

這些『前提』的邏輯判斷有各式各樣的可能,所以資料庫無法自動偵測,唯一的解決辦法就是確保 Transaction 間的資料更新有 Serializable 的特性,所以 Write Skew 現象只有在 Serializable Isolation (下一段說明) 可以避免。

Isolation Levels

Read Uncommitted Isolation

最低的 Isolation Level,允許讀取還沒有被 Commit 的資料,有可能發生 Dirty Read 和其他所有的現象。

Read Committed Isolation

只允許讀取已經被 Commit 的資料,可以避免 Dirty Read,但是其他四個現象還是有可能發生。

Repeatable Read Isolation

只要能夠避免 Dirty Read 和 Non-repeatable Read 現象就可以被稱為 Repeatable Read Isolation。依照實作的方法不同,有的 Repeatable Read Isolation 還可以避免 Lost Update 或 Phantom 現象。

Repeatable Read 有幾種實作的方法,其中一種是對讀取過的資料都加上 Shared Lock,一直到 Transaction 結束,期間都不允許其他 Transaction 做寫入更新。但是這一種實作方法因為沒有做 Range Lock,通常都無法避免 Phantom 現象。

另一種比較常見的實作方法是 Snapshot Isolation,在每個 Transaction 第一次讀取資料時,對資料庫做一個概念上像是 Snapshot 的紀錄。Transaction 之後就都只能看到這個 Snapshot 的內容,無法讀取到其他 Transaction 所做的更動,避免 Non-repeatable Read。包括 MySQL InnoDB 跟 PostgreSQL 都是採用 Snapshot Isolation 做為 Repeatable Read Isolation。

Serializable Isolation

Serializable Isolation 可以保證在多個 Transaction 同時對資料庫進行讀寫所得到的結果,會跟一次只讓一個 Transaction 照順序 (serially) 進行讀寫所得到的結果完全一致。Serializable 通常被認為是最嚴格的 Isolation Level,可以避免上述全部五種現象,但是因為必須犧牲一些 Concurrency,效能較差。

Serializable Isolation 有三種實作方式:

  1. Serial Order:真正的照順序 (serially) 進行資料讀寫,避免所有同時執行更新可能造成的衝突。缺點是犧牲了所有 Concurrency,效能差,適合讀寫較快速的 In-Memory 資料庫採用,例如 Redis。
  2. Two-phase Lock:使用 Shared Lock 和 Exclusive Lock 的搭配,讓每一筆資料在同一時間最多都只會有一個 Transaction 對它進行讀取和更新。另外還要搭配 Range Lock 來避免 Phantom 現象。大部分的資料庫都採用這種實作。
  3. Serializable Snapshot Isolation (SSI):與其他實作不同的是,SSI 採用Optimistic Concurrency Control,樂觀的認為發生衝突現象的機率其實很小,所以,與其對資料做 Shared/Exclusive Lock 讓 Transaction 互相 Block,不如讓所有 Transaction 都正常執行,在最後的 Commit 階段再做檢查,看是否有違反 Isolation 規則的衝突發生。因此 SSI 對效能的影響很小,但是因為它在 2008 年才被提出,目前大部分的資料庫實作都還沒有採用。PostgreSQL 從 9.1 之後才開始採用。

Chester Chu

Written by

A Software Engineer, Gopher and Full Stack Developer.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade