事務隔離級別,看這一篇就夠了

簡介:作者:旺德 / 孟勃榮

5-6-3.gif

談到事務隔離級別,開發同學都能說個八九不離十。髒讀、不可重複讀、RC、RR...這些常見術語也大概知道是什麼意思。但是做技術,嚴謹和細緻很重要。如果對事務隔離級別的認識,僅僅停留在大概知道的程度,數據庫內核研發者可能開發出令用戶費解的隔離級別表現,業務研發者可能從數據庫中查出與預期不符的結果。

那麼如何判斷自己是不是對事務隔離級別有了較爲深入的理解了呢?開發同學可以問自己這樣兩個問題:(1)事務隔離級別分爲幾類?分別能解決什麼問題?是否有明確定義?這樣的定義是否準確?(2)當前主流數據庫(Oracle/MySQL...)的隔離級別表現和實現是怎樣的?是否與“官方”定義一致?

如果能清楚明白的回答這兩個問題,恭喜,你對事務隔離級別認識已經非常深刻了。如果不能,也沒有關係,讀完本文你就有答案了。

1.事務隔離級別

事務隔離級別,主要保障關係數據庫ACID特性的I(Isolation),既針對存在衝突的併發事務,提供一定程度的安全保證。ANSI(American National Standards Institute) SQL 92標準(http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt) 首先定義了3種併發事務可能導致的不一致異象:

Dirty read: SQL-transaction T1 modifies a row. SQL- transaction T2 then reads that row before T1 performs a COMMIT. If T1 then performs a ROLLBACK, T2 will have read a row that was never committed and that may thus be considered to have never existed.
Non-repeatable read: SQL-transaction T1 reads a row. SQL- transaction T2 then modifies or deletes that row and performs a COMMIT. If T1 then attempts to reread the row, it may receive the modified value or discover that the row has been deleted.
Phantom: SQL-transaction T1 reads the set of rows N that satisfy some . SQL-transaction T2 then executes SQL-statements that generate one or more rows that satisfy the used by SQL-transaction T1. If SQL-transaction T1 then repeats the initial read with the same , it obtains a different collection of rows.

嫌棄以上定義冗長,可以直接看以下形式化描述:

A1 Dirty Read:w1[x] ... r2[x] ... (a1 and c2 in any order)
A2 Fuzzy Read:r1[x] ... w2[x] ... c2 ... r1[x] ... c1
A3 Phantom Read:r1[P] ... w2[y in P] ... c2 ... r1[P] ... c1

其中w1[x]表示事務1寫入記錄x,r1表示事務1讀取記錄x,c1表示事務1提交,a1表示事務1回滾,r1[P]表示事務1按照謂詞P的條件讀取若干條記錄,w1[y in P]表示事務1寫入記錄y滿足謂詞P的條件。

據此,ANSI定義了四種隔離級別,分別解決以上三種異常:
image.png
根據上述幾種異常現象定義隔離級別,可謂十分不嚴謹,Jim Gray大名鼎鼎的論文A Critique of ANSI SQL Isolation Levels(後文簡稱Critique)就對此做了批判。

不嚴謹之一:禁止了P1/P2/P3的事務,即滿足了Serializable級別。但是在ANSI標準中又明確描述Serializable級別爲“多個併發事務執行的效果與某種串行化執行的效果等價”。顯然這兩者是矛盾的,禁止P1/P2/P3的事務,不一定能滿足“等價於某種串行執行”。所以Critique將ANSI定義的禁止了P1/P2/P3的隔離級別稱爲Anomaly Serializable。

不嚴謹之二:異常現象定義不準確,如下例並未被A1囊括,卻仍然出現了Dirty Read(Txn2讀到x+y!=100)。同樣,A2/A3也能舉出這樣的例子,感興趣的同學可以自己嘗試列舉,這裏不再詳述。

image.png

究其原因,ANSI對異象的定義太爲嚴格,如果除去對事務提交、回滾和數據查詢範圍的要求,僅保留關鍵的併發事務之間讀寫操作的順序,更爲寬鬆且準確的異象定義如下:

P1 Dirty Read: w1[x]...r2[x]...(c1 or a1)
P2 Fuzzy Read: r1[x]...w2[x]...(c1 or a1)
P3 Phantom: r1[P]...w2[y in P]...(c1 or a1)

不嚴謹之三:三種異象僅針對S(ingle) V(alue)系統,不足以定義M(ulti)V(ersion)系統的隔離性。很多商業數據庫所實現的SI,未違反P1、P2和P3,但又可能出現Constraint violation,不可串行化。除了P1/P2/P3,還可能出現哪些異常呢?

P4 Lost Update:r1[x]...w2[x]...w1[x]...c1
A5A Read Skew:r1[x]…w2[x]... w2[y]…c2…r1[y] …(c1 or a1)
A5B Write Skew:r1[x]…r2[y]…w1[y]…w2[x]…(c1 and c2 occur)
A5B2 Write Skew2:r1[P]... r2[P]…w1[y in P]…w2[x in P]...(c1 and c2 occur)

對這四種情況,分別舉一個例子:

r1[x=50] r2[x=50] w2[x=60] c2 w1[x=70] c1

Lost Update:事務1和事務2同時向同一個賬戶x分別充20和10塊,事務1後提交,將70塊寫入數據庫,事務2提交結果60塊被覆蓋。正確的情況下,事務1和2提交成功,賬戶裏應該有80塊。

(x+y=100) r1[x=50] w2[x=10] w2[y=90] c2 r1[y=90] c1

Read Skew: x和y賬戶分別有50塊錢,加起來共100塊。事務1讀x(50塊)後,事務2將x賬戶的40塊轉到y賬戶,事務2提交後,事務1讀y(90塊)。在事務1看來,x+y=140,出現了不一致。

(x+y>=60) r1[x=50] r2[y=50] w1[y=10] c1 w2[x=10] c2

Write Skew:x和y賬戶分別有50塊錢,加起來共100塊。假設存在某種約束,x和y賬戶的錢加起來不得少於60塊。事務1和事務2在自認爲不破壞約束的情況下(分別讀了x賬戶和y賬戶),再分別從y賬戶和x賬戶取走40。但事實上,這兩個事務完成後,x+y=20,約束條件被破壞。

(count(P)<=4):r1[count(P)=3],r2[count(P)=3],insert1[x in P],insert2[y in P],c1,c2,

Write Skew2:將Write Skew的條件改爲範圍。

2.隔離級別實現

上一節介紹了ANSI定義的3種異象,及根據禁止異象的個數而定義的事務隔離級別。因爲不存在嚴格、嚴謹的“官方”定義,各主流數據庫隔離級別的表現也略有不同,一些現象甚至讓用戶感到困惑。我認爲相較於糾結隔離級別的準確定義,認識各數據庫隔離級別的表現和實現,在生產環境中正確的使用它們纔是更應該關注的事情。本節將以大篇幅具體的例子爲切入點,介紹幾種主流數據庫隔離級別的表現,及內部對應的實現。

2.1 Lock-based 隔離級別實現

在展示Lock-based隔離級別實現前,先介紹幾個與鎖相關的概念:

Item Lock:對訪問行加鎖,可以防止dirty/fuzzy read。
Predicate Lock(gap lock):對search的範圍加鎖,全表掃描直接對整張表加鎖,可防止phantom read。
Short duration:語句結束後釋放鎖。
Long duration:事務提交或回滾後釋放鎖。

上述鎖操作組合,便可實現不同級別的事務隔離標準,如下表所示。
image.png
其中S lock代表共享鎖,X lock代表排它鎖。

首先所有寫操作加X locks時,都會選擇Long duration,否則short duration鎖被釋放後,在事務提交前該條更改可能被其它事務寫操作覆蓋,造成髒寫(dirty write)。

其次對於讀操作:

Short duration Item S lock 禁止了 P1發生,讀操作如果遇到正在修改的行(寫事務加了X Lock),阻塞在S Lock,直到寫事務提交。

Long duration Item S lock 禁止了P2發生,寫操作遇到讀事務(S Lock),阻塞在X Lock上直到讀事務提交或回滾。

Long duration Predicate/Table S Lock 禁止了P3發生,(範圍)寫操作遇到範圍讀操作(加Predicate S Lock),會被阻塞,直到讀事務提交或回滾。

基於鎖實現的三種隔離級別分別能禁止的異象如下表所示:

image.png

然而當今數據庫基於性能等多方面考慮,很少有完全基於鎖實現隔離級別的,MVCC+Lock的方式,可以滿足讀請求不加鎖,是主流的實現方式。

2.2 Oracle隔離級別的實現

Oracle僅支持兩種隔離級別:Read Committed與Serializable。儘管官方這樣描述,Oracle的Serializable實際是基於MVCC+Lock based的SI(Snapshot Isolation)隔離級別。

爲實現快照讀,內部維護了全局變量SCN(System Commit/Change Number),在事務提交時遞增。讀請求獲取Snapshot便是獲取當前最新的SCN。Oracle實現MVCC的方式是將block分爲兩類:(1)Current blocks爲當前最新的頁面,與持久化態數據保持一致。(2)Consistent Read blocks,根據snapshot SCN生成相應的一致性版本頁面。

以下兩個具體的例子展示了:不同隔離級別下,讀寫語句在數據庫內部發生了什麼。
image.png

Oracle在read committed隔離級別下,每條語句都會獲取最新的snapshot,讀請求全部是snapshot讀。寫請求在更新行之前,需要加行鎖。由於寫操作不會因爲有其它事務更新了同一行,而停止更新(除非不滿足更新的謂詞條件了),因此Lost Update有可能發生。
image.png

Oracle在serializable隔離級別下,事務開始便獲取snapshot。讀請求全部是snapshot讀,而寫請求在更新行之前,需要加行鎖。寫操作在加鎖後,首先檢查該行,如果發現:最近修改過這行的事務的SCN大於本事務的SCN,說明它已經被修改且無法被本事務看到,會做報錯處理,避免了Lost Update。這種寫衝突的實現,顯然是first committer wins。

下表展示了Oracle的兩種隔離級別,分別能夠避免哪些異象:

image.png

2.3 MySQL(InnoDB)隔離級別實現

InnoDB同樣以MVCC+Lock的方式實現隔離級別。其中普通select語句均是snapshot read。而delete/update/select for update等語句是加鎖實現的current read,如下表所示(注:該表爲Pecona 5.6版本的代碼實現)。

image.png

InnoDB的RC隔離級別的表現與Oracle相似。而相較於Oracle的SI,InnoDB RR隔離級別依舊不能避免Lost Update(例如下例)。究其原因,InnoDB在RR隔離級別下,不會在事務提交時判斷是否有其它事務修改過該行。這避免了了SI更新衝突帶來的回滾代價,帶來了可能發生Lost Update的風險。

image.png

由於update等操作均是加鎖的當前讀,因此Phantom Read的現象也是存在的(如下表所示)。但是如果將Txn1的update語句替換爲select語句,Phantom Read現象則可以禁止,因爲整個事務select語句使用的是同一個snapshot。

image.png

Innodb RR的實現方式雖然並非並未嚴格排除Lost Update和Repeatable Read,但其充分利用MVCC讀不加鎖的併發能力,同時current read避免了SI在更新衝突劇增時過多的回滾代價。

InnoDB還實現了Lock Based Serializable(詳見2.1),禁止了所有異象。

3.MySQL (X-Engine) 隔離級別實現

X-Engine 隔離級別實現同樣採用MVCC+Lock的方式,支持RC和SI,表現與Oracle的RC,Serializable一致。具體實現層面,X-Engine 實現了行級MVCC,每條記錄的key都附有一個 Sequence 代表自己的版本。所有的讀操作均是快照讀(包括加鎖讀),讀請求所需要的snapshot也是一個Sequence 。寫寫衝突處理依靠兩階段鎖,並遵循First committer wins。

按照慣例,以下面兩個例子分析,說明我們的實現原理:
image.png
image.png
與Oracle類似,X-Engine SI隔離級別,可以避免Lost Update:
image.png

4.總結

image.png

前文介紹了多種數據庫隔離級別的表現,對比如上表所示。其種MySQL比較特殊,如前文所述,其RR級別可以禁止部分幻讀現象。開發人員在使用數據庫時,需要注意:儘管不同數據庫隔離級別名稱相同,但是表現卻可能存在差異。

原文鏈接:https://developer.aliyun.com/article/766148?

版權聲明:本文中所有內容均屬於阿里雲開發者社區所有,任何媒體、網站或個人未經阿里雲開發者社區協議授權不得轉載、鏈接、轉貼或以其他方式複製發佈/發表。申請授權請郵件[email protected],已獲得阿里雲開發者社區協議授權的媒體、網站,在轉載使用時必須註明"稿件來源:阿里雲開發者社區,原文作者姓名",違者本社區將依法追究責任。 如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至:[email protected] 進行舉報,並提供相關證據,一經查實,本社區將立刻刪除涉嫌侵權內容。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章