事務前沿研究 | 隔離級別的追溯與究明,帶你讀懂 TiDB 的隔離級別(下篇)

緒論

在上篇,我們分析了 ANSI SQL-92 和「A Critique of ANSI SQL Isolation Levels」對隔離級別做出的定義,並且指出了在現今的認知中,其中的一些缺陷。本篇將繼續討論隔離級別的問題,講述實現無關的隔離級別定義和 TiDB 的表現和隔離級別。

Generalized Isolation Level Definitions

介紹

上文所講的「A Critique of ANSI SQL Isolation Levels」這篇文章在定義隔離級別的時候,對事務的過程也提出了諸多的要求,然而「Generalized Isolation Level Definitions」僅對成功提交的事務做了約束,即所有異常現象都是由成功提交的事務產生的。在例 1-a 中,因爲 T1 沒有成功提交,所以並沒有出現異常,而例 1-b 中 T1 讀到了 abort 事務 T2 的寫入內容並且提交成功了,產生了異常現象(G1a - Aborted Read)。

Txn1 Txn2
w(x, 1)
r(x, 1)
abort
abort

例 1-a - 提交是出現異常的必要條件

Txn1 Txn2
w(x, 1)
r(x, 1)
abort
commit

例 1-b - 提交是出現異常的必要條件

「Generalized Isolation Level Definitions」提出了與實現無關的隔離級別定義,並且更清晰的解釋了 predicate 和 item 現象所帶來的異常區別,提出了對標 ANSI SQL-92 的隔離級別。

依賴圖

Adya 首先引入了三類依賴,可以簡單的概括爲寫讀(WR),讀寫(RW)和寫寫(WW)。含有讀的依賴按照讀操作的 item 和 predicate 查詢類別被細分爲兩種類型,item 指的是在一個 key 之上產生的依賴;而 predicate 則是指改變了一個 predicate 結果集,包括改變其中某個 item 的值和改變某個 item 在 predicate 下的命中狀態。

兩個事務間存在依賴則一定程度上代表了兩個事務在現實時間中的先後關係,如果兩個依賴中分別出現了 T1 先於 T2 和 T2 先於 T1 的現象,那麼就證明出現了事務在現實事件中交叉出現的現象,破壞了 Serializable,這是本篇論文的核心觀點。

Read Dependencies (WR)

WR 依賴指的是爲 T2 讀到了 T1 寫入的值。

例 2 是針對單個 key 的 WR 依賴,T2 讀到了 T1 寫入的值,稱爲 Directly item-read-depends。

Txn1 Txn2
w(x, 1)
r(x, 1)
commit
commit

例 2 - Directly item-read-depends

例 3 是 predicate 條件下的 WR 依賴,例 3-a 是將一個 key 從符合 predicate 條件改爲了不符合條件,而例 3-b 是將一個 key 從不符合 predicate 條件改爲了符合條件。

Txn1 Txn2
r(x, 1)
w(x, 10)
r(sum(x)|x<10)
commit
commit

例 3-a - Directly predicate-read-depends

Txn1 Txn2
r(x, 10)
w(x, 1)
r(sum(x)|x<10)
commit
commit

例 3-b - Directly predicate-read-depends

Anti-Dependencies(RW)

WR 依賴指的是爲 T2 修改了 T1 讀到的值。

例 4 是針對單個 key 的 RW 依賴,T1 在 T2 讀到的 key 之上寫入了新值,稱爲 Directly item-anti-depends。

Txn1 Txn2
r(x, 1)
w(x, 2)
commit
commit

例 4 - Directly item-anti-depends

例 5 是 predicate 條件下的 WR 依賴,例 5-a 是將一個 key 從符合 predicate 條件改爲了不符合條件,而例 5-b 是將一個 key 從不符合 predicate 條件改爲了符合條件。

Txn1 Txn2
r(x, 1)
r(sum(x)|x<10)
w(x, 10)
commit
commit

例 5-a - Directly predicate-anti-depends

Txn1 Txn2
r(x, 10)
r(sum(x)|x<10)
w(x, 1)
commit
commit

例 5-b - Directly predicate-anti-depends

Write Dependencies(WW)

WW 依賴指的是兩個事務寫了同一個 key,例 6 中 T1 寫入了 x 的第一個值,T2 寫入了 x 的第二個值。

Txn1 Txn2
w(x, 1)
w(x, 2)
commit
commit

例 6 - Directly Write-Depends

DSG

DSG (Direct Serialization Graph) 可以被稱爲有向序列化圖,是將對一系列事務進行以來分析後,將上述的三種依賴作爲 edge,將事務作爲 node 繪製出來的圖。圖 1 展示了從事務歷史分析得到 DSG。如果 DSG 是一個有向無環圖(如圖 1 所示),那麼這些事務間的依賴關係所決定的事務先後關係不會出現矛盾,反之則代表可能有異常,這篇文章根據出現異常時組成環的 edge 的依賴類型,定義了隔離級別。

圖 1 - 從事務歷史分析 DSG

異常現象與隔離級別

爲了不和「A Critique of ANSI SQL Isolation Levels」產生符號上的衝突,這篇文章使用 G 表示異常現象,使用 PL 表示隔離級別。

PL-1 & G0

G0 (Write Cycles) 和類似於髒寫定義,但要求 P0 (Dirty Write) 現象實際產生異常,如果僅僅是兩個事務寫同一個 key 並且並行了,他們還是可以被視爲 Serializable,只有當兩個事務互相出現依賴的時候才屬於 G0 現象。例 7-a屬於 P0 現象,但只看這個現象本身,是符合 Serializable 的,而例 7-b 同時發生了 P0 和 G0。

Txn1 Txn2
w(x, 1)
w(x, 2)
commit commit

例 7-a - P0 (Dirty Write) 與 G0 對比 - P0

Txn1 Txn2
w(x, 1)
w(x, 2)
w(y, 1)
w(y, 2)
commit commit

例 7-b - P0 (Dirty Write) 與 G0 對比 - G0

如果不會出現 G0 現象,則達到了 PL-1 的隔離級別。

PL-2 & G1

G1 現象有三條,其中 G1a 和 G1b 與依賴圖無關,G1c 是依賴圖上的異常。

G1a (Aborted Reads) 指讀到了中斷事務的內容,例 8 是 G1a 現象的兩種情況,不管是通過 item 類型還是 predicate 類型的查詢讀到了中斷事務的內容,都屬於 G1a 現象。例 8-a 中,T1 將 x 寫爲 2,但是這個事務最後產生了 abort,而 T2 讀到了 T1 寫入的結果,產生了 G1a 現象;在例 8-b 中 T1 將 x 從 1 改寫爲 2,此時 sum 的值也會因此從 10 變爲 11,但是因爲 T1 最後產生了 abort,所以 T2 讀取到 sum 爲 11 的值也屬於 G1a 現象。

Txn1 Txn2
r(x, 1)
w(x, 2)
r(x, 2)
abort commit

例 8-a - G1a 現象

Txn1 Txn2
r(x, 1)
r(sum, 10)
w(x, 2)
r(sum, 11)
abort commit

例 8-b - G1a 現象

G1b (Intermediate Reads) 指讀到了事務的中間內容,例 9 是 G1b 的兩種情況,item 類型和 predicate 類型的讀取都屬於 G1b 現象。在例 9-a 中,T1 將 x 從 1 修改爲 2,最後修改爲 3,但是對於其他事務而言,只能觀察到 T1 最後修改的值 3,所以 T2 讀取到 x=2 的行爲屬於 G1b 現象;在例 9-b 中,T2 雖然沒有直接從 T1 讀取到 x=2 的值,但是其讀取到的 sum=11 也包括了 x=2 的結果,其結果而言仍然讀取到了事務的中間狀態,屬於 G1b 現象。

Txn1 Txn2
r(x, 1)
w(x, 2)
w(x, 3)
r(x, 2)
commit commit

例 9-a - G1b 現象

Txn1 Txn2
r(x, 1)
r(sum, 10)
w(x, 2)
w(x, 3)
r(sum, 11)
commit commit

例 9-b - G1b 現象

G1c (Circular Information Flow) 指 WW 依賴和 WR 依賴組成的 DSG 中存在環,圖 2 描述了 G1c 現象,這個例子可以理解爲,T1 和 T2 同時寫了 x,並且 T2 是後寫的,所以 T2 應該晚於 T1 提交,同理 T3 應該晚於 T2 提交。而最後 T1 讀到了 T3 寫入的 z = 4,所以 T3 需要早於 T1 提交,發生了矛盾。

圖 2 - G1c 現象

如果不會出現 G0 和 G1 的三個子現象,則達到了 PL-2 的隔離級別。

PL-3 & G2

G2 (Anti-dependency Cycles) 指的是 WW 依賴、WR 依賴和 RW 依賴組成的 DSG 中存在環,圖 3 展示了對上篇的 Phantom 現象進行分析,在其中發現 G2 現象的例子。在這個例子中,如果 T1 或者 T2 任意一個事務失敗,或者 T1 沒有讀取到 T2 寫入的值,那麼實際上就不存在 G2 現象也不會發生異常,但是根據 P3 的定義,Phantom 現象已經發生了。本文認爲 G2 比 P3 用一種更加合理的方式來約束 Phantom 問題帶來的異常,同時也補充了 ANSI SQL-92 的 Phantom Read 必須要兩次 predicate 讀才能算作異常的不合理之處。

圖 3 - G2 現象

如果不會出現 G0、G1 和 G2 現象,則達到了 PL-3 的隔離級別。

PL-2.99 & G2-item

PL-3 的要求非常嚴格,而 PL-2 又相當於 Read Committed 的隔離級別,這就需要在 PL-2 和 PL-3 之間爲 Repeatable Read 找到位置。上篇提到過 Non-repeatable Read 和 Phantom Read 的區別在於是 item 還是 predicate 類型的讀取,理解了這一點之後,G2-item (Item Anti-dependency Cycles) 就呼之欲出了。

G2-item 指的是 WW 依賴、WR 依賴和 item 類型的 RW 依賴組成的 DSG 中存在環。圖 4 展示了對 Non-repeatable 現象進行分析,在其中發現 G2-item 現象的例子。

圖 4 - G2-item 現象

如果不會出現 G0、G1 和 G2-item 現象,則達到了 PL-2.99 的隔離級別。

小結

表 1 給出了與實現無關的隔離級別定義,圖 5 將其與「A Critique of ANSI SQL Isolation Levels」所提出的隔離級別進行了對比,右側是這篇文章所給出的定義,略低於左側是因爲這一定義要求事務被提交才能夠產生異常。

G0 G1 G2-item G2
PL-1 x
PL-2 x x
PL-2.99 x x x
PL-3 x x x x

表 1 - Adya 的隔離級別定義

圖 5 - 隔離級別定義對比

這篇文章所做出的隔離級別的定義的優點在於:

  • 定義與實現無關;

  • 只約束了成功提交的事務,此前的定義限制了併發控制技術的空間,例如樂觀事務 “first-committer-wins” 的策略能夠被這一隔離級別更加好的解釋;

  • 指出了讀到事務中間狀態的異常;

  • 對 Phantom 現象提出了更加清晰和準確的定義;

  • 事務間的依賴關係和事務發生的順序無關,在這一定義下,更容易區分隔離性和線性一致性。

注意到前文所提到的 Snapshot Isolation 並沒有出現在這篇文章中,而如果分析 A5B - Write Skew 現象的話,會發現它其實是屬於 G2-item 現象的,這就導致很多 SI 的隔離級別只能被劃分到 PL-2 的隔離級別上。這是因爲這篇文章只對成功提交事務的狀態做出了規定,而在 Adya 的博士論文「Weak Consistency: A Generalized Theory and Optimistic Implementations for Distributed Transactions」中對事務的過程狀態也作出了約束,基於此提出了對事務中間狀態的補充,其中也包括 PL-SI 的隔離級別,本文關於此不再深入展開。

TiDB 的隔離級別

在這一節,我們將研究 TiDB 的行爲,TiDB 的悲觀事務模型和 MySQL 在行爲上十分相似,其分析可以類推到 MySQL 之上。

樂觀鎖與悲觀鎖

樂觀鎖和悲觀鎖是兩種加鎖技術,對應了樂觀事務模型和悲觀事務模型,樂觀鎖會在事務提交時檢查事務能否成功提交,“first-committer-wins” 的策略會讓後提交的衝突事務失敗,TiDB 會返回 write conflict 錯誤。因爲一個事務只要有一行記錄產生了衝突,整個事務都需要被回滾,所以樂觀鎖在高衝突的情況下會大幅度降低性能。

悲觀鎖則是在事務中的每個操作執行時去檢查是否會產生衝突,如果會產生衝突,則會重複嘗試加鎖行爲,直到造成衝突的事務中斷或提交。就算在無衝突的情況下,悲觀鎖也會增加事務執行過程中每個操作的延遲,這一點增加了事務執行過程中的開銷,而悲觀鎖則確保了事務在提交時不會因爲 write conflict 而失敗,增加了事務提交的成功率,避免了清理失敗事務的額外開銷。

快照讀與當前讀

快照讀和當前讀的概念在 MySQL 和 TiDB 中都存在。快照讀會遵循快照隔離級別的字面定義,從事務的快照版本讀取數據,一個例外情況是在快照讀下會優先讀取到自身事務修改的數據(local read)。當前讀能夠讀取到最新的數據,實現方式爲獲取一個最新的時間戳,將此作爲當前讀讀取的快照版本。Insert/update/delete/select for update 會使用當前讀去讀取數據,使用當前讀也經常被稱爲“隔離級別降級爲 Read Committed”。這兩種讀取方式的混合使用可能產生非常難以理解的現象。例 10 給出了在混合使用情況下,當前讀影響快照讀的例子,按照快照讀和當前讀的行爲定義,快照讀是不能看到事務開始後新插入的數據的,而當前讀可以看到,但是噹噹前讀對這行數據進行修改之後,這行數據就變爲了“自身事務修改的數據”,於是快照讀優先使用了 local read。

Txn1 Txn2
create table t(id int primary key, c1 int);
begin
select * from t; -- 0 rows
insert into t values(1, 1);
select * from t; -- 0 rows
update t set c1 = c1 + 1; -- 1 row affected
select * from t; -- 1 row, (1, 2)
commit;

例 10 - 混合使用快照讀與當前讀

讀時加鎖

在悲觀事務下,point get 和 batch point get 的執行器在使用當前讀時,TiDB 有特殊的讀時加鎖策略,執行流程爲:

  • 讀取數據並加鎖

  • 將數據返回給客戶端

相比之下,其他執行器在當前讀下的加鎖流程爲:

  • 讀取數據

  • 給讀取到的數據加鎖

  • 將數據返回給客戶端

如例 11 所示,他們的區別在於,讀時加鎖能夠鎖上不存在的數據索引(point get 和 batch point get 一定存在唯一索引),即使沒有讀到數據,也不會讓這個索引被其他事務所寫入。回顧一下 P2 - Fuzzy Read,這一行爲正好和 P2 的讀鎖要求一致,因此,悲觀事務下的當前讀配合讀時加鎖的策略能夠防止 Fuzzy Read 異常的發生。

create table t(id int primary key);
begin pessimistic;
select * from t where id > 1 for update; -- 0 rows returns, will not lock any key
select * from t where id = 1 for update; -- 0 rows returns, lock pk(id = 1)

例 11 - 混合使用快照讀與當前讀

RC 與讀一致性

RC 有兩種理解,一種是 ANSI SQL-92 中的 Read Committed,另一種是 Oracle 中定義的 Read Consistency。一致性讀要求讀取操作要讀到相同的內容,圖 6 是讀不一致的例子,在一個讀請求發生的過程中,發生了另一個事務的寫入,對 x 和 y 讀到了不同時刻的數據,破壞了 x + y = 100 的約束,出現了一致性問題,讀一致性能夠防止這種情況的發生。

圖 6 - 讀不一致

在 Oracle 中,讀一致性有兩個級別:

  • 語句級別

  • 事務級別

語句級別保證了單條語句讀一致性,而事務級別保證了整個事務的讀一致性。如果使用快照的概念來進行理解的話,語句級別的讀一致性代表每條語句會從一個快照進行讀取,而事務級別的讀一致性代表一個事務中的每一條語句都會從一個快照進行讀取,也就是我們常說的快照隔離級別。

TiDB 的 RC 實現了語句級別的讀一致性,並且保證每次讀取都能夠讀到最新提交的數據,從而實現了 Read Committed 的隔離級別。

異常分析

快照隔離級別通過多版本的方式來防止了 P0 可能會帶來的異常現象,Fuzzy Read 會因爲兩個情況發生:

  • 樂觀事務模型下不加讀鎖的當前讀

  • 混合使用快照讀和當前讀

而 Phantom 異常則會因爲不存在給 predicate 的加鎖行爲而出現。

綜上所述,如果只使用快照讀的話,TiDB 是不會出現 Phantom 或者 G2 異常的,但是快照讀因爲會出現 A5B (Write Skew),依舊會違反 G2-item,只有讀時加鎖能夠防止 A5B,需要因場景選擇事務模型才能夠取得理想的結果。

總結

在下篇中,我們解讀了實現無關的隔離級別定義,實現無關隔離級別定義的提出大大簡化了對事務隔離性的分析,同時也會作爲後續分析的基礎內容。最後我們分析了 TiDB 中的一些行爲,商業數據庫在實現時在遵循標準的同時又有着更復雜的,不管對於數據庫的開發者還是用戶,理解數據庫行爲的原因都是十分重要且有益的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章