
談到事務隔離級别,開發同學都能說個八九不離十。髒讀、不可重複讀、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定義了四種隔離級别,分别解決以上三種異常:
根據上述幾種異常現象定義隔離級别,可謂十分不嚴謹,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也能舉出這樣的例子,感興趣的同學可以自己嘗試列舉,這裡不再詳述。
究其原因,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:事務送出或復原後釋放鎖。
上述鎖操作組合,便可實作不同級别的事務隔離标準,如下表所示。
其中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),會被阻塞,直到讀事務送出或復原。
基于鎖實作的三種隔離級别分别能禁止的異象如下表所示:
然而當今資料庫基于性能等多方面考慮,很少有完全基于鎖實作隔離級别的,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生成相應的一緻性版本頁面。
以下兩個具體的例子展示了:不同隔離級别下,讀寫語句在資料庫内部發生了什麼。
Oracle在read committed隔離級别下,每條語句都會擷取最新的snapshot,讀請求全部是snapshot讀。寫請求在更新行之前,需要加行鎖。由于寫操作不會因為有其它事務更新了同一行,而停止更新(除非不滿足更新的謂詞條件了),是以Lost Update有可能發生。
Oracle在serializable隔離級别下,事務開始便擷取snapshot。讀請求全部是snapshot讀,而寫請求在更新行之前,需要加行鎖。寫操作在加鎖後,首先檢查該行,如果發現:最近修改過這行的事務的SCN大于本事務的SCN,說明它已經被修改且無法被本事務看到,會做報錯處理,避免了Lost Update。這種寫沖突的實作,顯然是first committer wins。
下表展示了Oracle的兩種隔離級别,分别能夠避免哪些異象:
2.3 MySQL(InnoDB)隔離級别實作
InnoDB同樣以MVCC+Lock的方式實作隔離級别。其中普通select語句均是snapshot read。而delete/update/select for update等語句是加鎖實作的current read,如下表所示(注:該表為Pecona 5.6版本的代碼實作)。
InnoDB的RC隔離級别的表現與Oracle相似。而相較于Oracle的SI,InnoDB RR隔離級别依舊不能避免Lost Update(例如下例)。究其原因,InnoDB在RR隔離級别下,不會在事務送出時判斷是否有其它事務修改過該行。這避免了了SI更新沖突帶來的復原代價,帶來了可能發生Lost Update的風險。
由于update等操作均是加鎖的目前讀,是以Phantom Read的現象也是存在的(如下表所示)。但是如果将Txn1的update語句替換為select語句,Phantom Read現象則可以禁止,因為整個事務select語句使用的是同一個snapshot。
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。
按照慣例,以下面兩個例子分析,說明我們的實作原理:
與Oracle類似,X-Engine SI隔離級别,可以避免Lost Update:
4.總結
前文介紹了多種資料庫隔離級别的表現,對比如上表所示。其種MySQL比較特殊,如前文所述,其RR級别可以禁止部分幻讀現象。開發人員在使用資料庫時,需要注意:盡管不同資料庫隔離級别名稱相同,但是表現卻可能存在差異。