概述mysql
數據庫相對於其它存儲軟件一個核心的特徵是它支持事務,所謂事務的ACID就是原子性,一致性,隔離性和持久性。其中原子性,一致性,持久性更可能是關注單個事務自己,好比,原子性要求事務中的操做要麼都提交,要麼都不提交;一致性要求事務的操做必須知足定義的約束,包括觸發器,外鍵約束等;持久性則要求若是事務成功提交了,不管發生什麼異常,包括進程crash,主機掉電等,都應該確保事務不會丟失。而隔離性,則關注的是多個事務之間的併發。git
若是全部的事務都串行執行,相互不影響,不會有隔離的級別的問題。可是,串行沒法充分發揮多核的優點,所以須要併發執行多個事務,而且「儘可能」作到併發執行的事務與串行執行等價。爲何是「儘可能」?是由於數據庫中實際上不僅有一種隔離級別,可串行化,因此纔有必要討論數據庫中的隔離級別。好比拿MySQL舉例,隔離級別包括,讀未提交,讀提交,可重複讀,和串行化4種,其中可串行化是最嚴格的隔離級別,意味着事務之間產生衝突的機率最高。理論上,只有「可串行化」的事務序列纔是「正確的」,可是,因爲數據庫系統須要追求更好的性能,更高的系統吞吐,因此係統中會定義另外「比較弱」的隔離級別。每種「弱」的隔離級別定義,都會明確說明它會產生哪些「異常」,若是用戶能容忍這些「異常」,很好,那麼咱們不用將數據庫設置爲最嚴的併發控制模式。因此,簡單來講,經過隔離級別的設置,用戶能夠在「異常」和數據庫性能之間作一個權衡。github
數據庫中異常sql
本文討論的隔離級別主要源於論文A Critique of ANSI SQL Isolation Levels,論文中定義了一系列「異常」,而且說明了不一樣的隔離級別分別解決了哪些「異常」。說明下文中,w[n]表示事務n寫,r[n]表示事務n讀,a[n]表示事務n-abort,c[n]表示事務n-commit。A0,P1,P2,P3,A4,A5等異常命名編號均來源於論文。數據庫
1.髒寫併發
A0,dirty-write(WW),髒寫iphone
訪問模式:w1[x], w2[x],c1,c2性能
兩個事務前後寫x,這種會致使w2事務覆蓋w1的寫。spa
2.髒讀3d
P1,dirty-read(WR),髒讀
訪問模式:w1[x], r2[x],a1,c2
事務2讀到的x值,而最終事務1 abort了,這個x值根本不該該存在。
P1是區分Read Uncommitted和Read Committed隔離級別
3.不可重複讀
P2,Non-repeatable Read【Fuzzy Read】
訪問模式,r1[x],w2[x],w2[commit],r1[x]
事務r1兩次訪問x,返回的結果不同。好比x=10,
r1[x=10],w2(x=50),w2[commit],r1[x=50]
事務r1兩次讀取x,讀到了不一樣的值。
P2用於區分ReadCommitted和RepeatableRead隔離級別。
4.幻讀
P3,Phantom
異常:同一個事務,兩次讀返回的結果集不同,
這裏主要是說的幻讀,幻讀比不可重複讀要求更嚴格,即事務內的任何一個查詢,都不該該受其餘事務的更新操做影響(insert,update,delete),而出現結果不一致的現象。好比說,第一個查詢select... where x>1 返回了3條記錄(3,4,5);在這個時候,有另外的一個事務insert x=6;當再次查詢時,發現x>1返回了(3,4,5,6)4條記錄,這個就是幻讀現象的一種。
P3用於區別Repeatable Read和Serializable。
P1--P3是傳統的根據異常區分而定義的隔離級別,讀提交,可重複讀,串行化。但這種分法描述的異常可能還不夠多和完整,特別是對於廣泛普遍流行的MVCC併發控制,因而論文中在標準隔離級別基礎上將「異常」定義地更豐富,而且詳細介紹了目前Snapshot-Isolation。
5.Lost Update(寫覆蓋)
A4, Lost Update
A4的訪問模式r1[x], w2[x], w2[commit], w1[x], w1[commit]
這種訪問模式下,w2的更新可能會丟失。由於w1可能基於一個比較old-x來作更新x的操做。
6.Read&Write Skew
A5, (Constraint Violation),考慮到兩個相關聯記錄x,y,知足x+y=100,根據讀寫能夠分爲兩種
A5A, Read Skew
r1[x]...w2[x]...w2[y]...c2...r1[y]...(c1 or a1)
事務1讀取x後,事務2同時更新了x,y而後commit,那麼事務1再讀取y。
x=50, y=50
r1[x=50]...w2[x=20]...w2[y=80]...c2...r1[y=80]...(c1 or a1)
那麼對於事務1,x+y=130
A5B, Write Skew(讀後寫)
A5B: r1[x]...r2[y]...w1[y]...w2[x]...(c1 and c2 occur)
C(x,y)知足x+y >= 0, x=10, y=0
r1[x=10,y=0],r2[x=10,y=0],w1[y=-10],w2[x=0],w1(commit),w2(commit)
最終結果是x=0,y=-10,致使不知足x+y>=0的約束
數據庫的隔離級別
咱們談隔離級別,其實是在談併發控制。一般數據庫實現併發控制主要有兩類,基於鎖的悲觀併發控制(2PL)和樂觀併發控制(OCC)。前者在操做數據的過程當中加鎖,直到事務提交時才釋放。後者在事務讀寫的過程當中不加鎖,而是在提交的時候經過對比操做的readset和writeset來判斷事務是否存在衝突,來決定是否提交。原始的基於鎖的悲觀併發控制,讀和寫都加鎖,併發度比較低,所以目前主流的數據庫系統都引入了多版本併發控制機制(MVCC),所謂MVCC,簡單來講,經過冗餘歷史版本,達到讀不加鎖,讀寫不互斥的目的,這種讀就是快照讀,區別於加鎖模式的當前讀。這一改進大大提交的整個數據庫系統的併發度,固然,若是要實現可串行化隔離級別,須要作額外的工做來保證。下面簡單討論下不一樣隔離級別下,分別有哪些異常,以及主流數據庫的實現方式。
1.READ UNCOMMITTED
讀寫都不加鎖,數據庫徹底不作併發控制,基本上沒什麼實用價值。
2.READ COMMITTED
寫記錄加鎖,讀基於快照讀,而且事務中每一個語句有獨立的快照,確保讀到最新的事務提交,解決了髒讀的問題,但不解決可重複讀問題,固然也沒法避免幻讀,ReadSkew&WriteSkew等問題。
3.REPEATABLE READ
提到REPEATABLE READ隔離級別,不得不提到SNAPSHOT,通常主流數據庫裏面都不提SNAPSHOT隔離級別,可是實際實現的時候又都是基於SNAPSHOT來作的,但這裏又有一些細微的區別。對於MySQL(InnoDB)而言,讀的時候仍然是快照讀,相對於READ-COMMITED隔離級別,是一個事務一個快照,確保可重複讀,也不存在幻讀問題;可是寫的時候,採用的當前讀,也就是更新的時候,再也不考慮快照,而是基於最新的版原本更新,這樣就可能會形成LostUpdate問題。固然,解決辦法也很簡單,事務內的讀也採用當前讀,這樣也就避免了LostUpdate問題。這裏舉個例子:假設t是一張庫存表,pk='iphone'是主鍵,賣出一部iphone就減去一個庫存,count=count-1;假設有兩種寫法
case1: begin: select var = count from t where pk = 'iphone'; var = var - 1; update count = var from t where pk = 'iphone'; commit; case2: begin: update count = count - 1 from t where pk = 'iphone'; commit;
對於case1,就會發生LostUpdate,試想下若是兩個同類型的事務併發,快照讀讀到的是old count,就可能出現覆蓋寫的問題,致使庫存少減了。
對於case2,則不會有LostUpdate問題,update場景下,讀都是當前讀,在RR隔離級別下,會加寫鎖,確保能讀到最新的count。
對於MySQL(RocksDB)而言,讀同樣是基於同一個快照;寫的時候,仍然是基於快照讀(這個與RocksDB的LSM存儲結構有關,只能基於一個快照去讀取多版本數據),那麼要更新記錄時候,會判斷記錄中的版本是否比事務的快照版本新(ValidateSnapshot),若是是,說明在事務獲取快照後,有其它事務執行了更新操做,這個時候事務會回滾,也就不會發生LostUpdate問題。PG也是採用相似的機制,與MySQL(InnoDB)的本質區別在於,寫的時候,是基於快照讀去寫,而仍是基於當前讀去寫。最終的效果是,MySQL(InnoDB)在RR隔離級別下,也會存在LostUpdate問題,同時由於快照讀和當前讀混用(select, select ... for update),實際上嚴格來講,也就沒有解決幻讀和可重複讀的問題。Oracle沒有實現RR隔離級別,只提供RC和SERIALIZABLE隔離級別。不管是MySQL(InnoDB,RocksDB),PG都沒有解決WriteSkew問題。
4.SERIALIZABLE
最嚴格的隔離級別,天然是沒有「異常」的,咱們前面也說到,爲了提供系統的併發度,才選擇經過下降數據庫的隔離級別,但必須要容忍部分「異常」。串行化解決了髒讀/寫,丟失更新,幻讀,不可重複讀,以及ReadSkew&WriteSkew等問題。MySQL(Innodb)經過將全部全部讀都變爲當前讀,並結合(GAP,Next-Key,InsertIntention)lock來實現串行化隔離,PG則是事務提交時,根據readset和writeset檢查是否與其它事務之間有讀寫依賴成環,最終肯定事務可否提交。MySQL(Rocksdb)只支持RC和RR,不支持串行化隔離級別。下圖來源於論文,整理了不一樣隔離級別對應的異常。
總結
本文結合論文和主流的數據庫系統討論了數據庫的隔離級別。通常來講,生產環境中設置ReadCommit的居多,文章中也提到了,在讀提交隔離級別下,會存在有不可重複讀,幻讀以及Read/Write Skew等問題。說明,生產環境是能夠「容忍」這些「異常」的。固然,這不能說明隔離級別不重要,若是某些業務場景,不能容忍「異常」,就好比我文章中提到的減庫存的例子,若是業務代碼寫法不正確,就可能致使問題。總之,咱們須要在系統的併發度和隔離級別作一個權衡,確保業務正確的前提下,獲得最好的性能。
參考文檔