事務的ACID和四個隔離級別

在實際的業務場景中,并發讀寫引出了事務控制的需求。主要關注事務的ACID和隔離性的4個級別。

ACID

事務指"一個被視為單一的工作單元的操作序列"。一個良好的事務處理系統,必須具備四個標準特性,即ACID

  1. 原子性(Atomicity):一個事務必須被視為一個不可分割的最小工作單元,整個事務中的所有操作要么全部提交成功,要么全部失敗回滾,對于一個事務來說,不可能只執行其中的一部分操作。
  2. 一致性(Consistency):數據庫總是從一個一致性的狀態轉換到另一個一致性的狀態。
  3. 隔離性(Isolation):通常來說,一個事務所做的修改在最終提交以前,對其他事務是不可見的。針對不同的業務需求,隔離性分為4個級別:讀未提交、讀已提交、可重復讀、串行化。見下
  4. 持久性(Durability):通常來說,一旦事務提交,則其所做的修改會永久保存到數據庫(即使系統崩潰,修改的數據也不會丟失)。針對不同的業務需求,持久性也分為多個級別,此處略。

隔離性的4個級別

在理解隔離性級別時,很容易混淆“幻讀”與“不可重復讀”的問題。這里先對4個隔離性級別給出概覽;然后分析原理,從實現角度理解各種問題;最后作出總結。

概覽

關注隔離性的4個級別,包括讀未提交(Read Uncommitted)、讀已提交(Read Committed)、可重復讀(Repeatable Read)、可序列化(Serializable);及其對應的問題,包括臟讀(Dirty Read)、不可重復讀(Nonrepeatble Read)、幻讀(Phantom Read)。

讀未提交

讀未提交是數據庫應保證的最低的隔離性級別:事務中的修改,即使沒有提交,對其他事務也都是可見的

讀未提交面臨臟讀的問題:事務可以讀取未提交的數據,而該數據可能在未來因回滾而消失。從性能上來說,讀未提交不會比其他的級別好太多,但卻缺乏其他級別的很多好處。除非真的有非常必要的理由,在實際應用中很少使用。

讀已提交

讀已提交滿足前面提到的隔離性的簡單定義:一個事務所做的修改在最終提交以前,對其他事務是不可見的。換句話說,一旦提交,該事務所作的修改對其他正在進行中的事務就是可見的

狹義上,讀已提交解決了臟讀的問題。這個級別有時候叫做不可重復讀,面臨不可重復讀的問題:兩次執行同樣的查詢,如果第二次讀到了其他事務提交的結果,則會得到不一樣的結果

大多數數據庫的默認隔離級別都是Read Committed,但MySQL不是。

可重復讀

在讀已提交的基礎上,可重復讀解決了部分不可重復的問題:同一個事務中多次讀取同樣記錄結果是一致的記錄指具體的數據行。

未能解決的那部分稱為幻讀當某個事務在讀取目標范圍內的記錄時,另一個事務又在該范圍內插入了新的記錄,當之前的事務再次讀取該范圍的記錄時,會產生第一次讀取范圍時不存在的幻行(Phantom Row)。需要注意的是,只有插入會產生幻行

MySQL的默認隔離級別是可重復讀,有幻讀問題。

可序列化

可序列化是最高的隔離級別:強制事務序列化執行

可序列化解決了幻讀問題。簡單來說,可序列化會在目標范圍加獨占鎖,將并發讀寫相同范圍數據的請求序列化。可序列化會導致大量的超時和鎖爭用問題,因此,實際應用中很少用到這個隔離級別,只有在非常需要確保數據的一致性而且可以接受沒有并發的情況下,才考慮采用該級別。

原理

討論基于鎖的并發控制中,4種隔離性級別的實現原理。重點關注讀鎖(read lock)、寫鎖(write lock,或稱排它鎖)、范圍鎖(range lock)、鎖的持有時間等概念。

讀未提交

讀未提交的實現:讀鎖、寫鎖都在一個原子操作(如select、insert等)完成后立即釋放。換句話說,事務作出更新后,不管是否提交,由于已經釋放了目標記錄的寫鎖,更新對其他事務就是可見的。

讀未提交存在臟讀問題,假設操作序列:

  1. 事務1開始
  2. 事務1讀取目標記錄
  3. 事務2開始
  4. 事務2修改目標記錄
  5. 事務1讀取目標記錄
  6. 事務2回滾
  7. 事務1提交

操作5中,事務1讀到了事務2修改但未提交的記錄,然后事務2回滾導致修改丟失,也就稱事務1讀到了“臟數據”,即臟讀。

區分目標記錄與目標范圍(見后文可重復讀的實現原理):

  • 目標記錄指一個具體的數據行;讀鎖、寫鎖只針對目標記錄。
  • 目標范圍指一個where語句描述的范圍;范圍鎖針對目標范圍,見后。

讀已提交

出現臟讀的原因是寫鎖的持有時間過短。讀已提交針對這一問題作出了優化:讀鎖仍然在一個原子操作完成后立即釋放;寫鎖從寫操作開始持有,事務提交后釋放。事務作出更新前,會先申請目標記錄的寫鎖,并持續持有至事務提交后,釋放鎖后,更新對其他事務才是可見的。

對于讀未提交中的操作序列,操作5發生時,由于事務2持有目標記錄的寫鎖,事務1會阻塞,直到事務2提交釋放該寫鎖,解決了臟讀問題。

讀已提交還存在不可重復讀問題。假設操作序列:

  1. 事務1開始
  2. 事務1讀取目標記錄
  3. 事務2開始
  4. 事務2修改目標記錄
  5. 事務2提交
  6. 事務1修改目標記錄
  7. 事務1提交

操作5完成后,事務2的修改對事務1可見,從而操作6中,事務1會讀到修改,與操作2的結果不同,因此修改結果無法保證(如根據操作2讀取的結果做修改);但是事務1在此之前未對目標記錄作出任何修改,因此事務1進行操作6時的狀態理應與操作2后一致(回顧事務的一致性要求)。以上即為不可重復讀。

不可重復讀與臟讀之間存在交叉。臟讀側重讀到不應存在的數據,不可重復讀強調兩次相同查詢的結果不一樣。實際上,可以將描述放寬到“目標記錄的狀態不符合預期狀態”,如本應該不同,卻讀到了相同。本質上也是由于讀已提交實現原理導致的問題。

可重復讀

解決不可重復讀可以使用兩種方法:

  1. 悲觀策略:串行化
  2. 樂觀策略:多版本 + 沖突檢測

悲觀策略:串行化

“串行化”不需要解釋,放棄并發、串行執行當然不存在任何問題。

“串行化”的可重復讀實現是:讀鎖、寫鎖從讀、寫操作開始持有,事務提交后釋放。與讀已提交的實現相比,可重復讀延長讀鎖的持有時間直到事務提交后,在此期間,目標記錄無法被修改。

對于讀已提交中的操作序列,操作2發生時,事務1開始持有目標記錄的讀鎖,導致事務2的操作4會陷入阻塞,直到事務1提交釋放鎖。

“串行化”不同于“可序列化”。為了區分,前、后文中均將隔離性級別稱為“可序列化”,將此處的悲觀策略稱為“串行化”。

樂觀策略:多版本 + 沖突檢測

“多版本 + 沖突檢測”是更常見的實現方案:多個事務采用多個版本,最后提交時檢測是否與當前數據版本沖突,如果沖突則報錯提醒,否則成功提交

“多版本 + 沖突檢測”的可重復讀實現是:事務開始時持有當前數據的快照,讀寫均不沖突,提交時檢測修改的快照與當前數據是否沖突。使用樂觀的沖突檢測策略代替悲觀的鎖策略,在中低程度的并發情況下性能更好。

對于讀已提交中的操作序列,事務1、2各自持有不同版本的快照,在操作4修改自己版本的目標記錄后,操作5提交事務2,檢測不沖突(假設沒有其他事務),合并到當前數據,當前數據完成修改;然后操作6繼續修改自己版本的目標記錄,操作7提交事務1,發現與當前數據沖突,給出報錯。

幻讀問題

幻讀是一種特殊的不可重復讀。

為什么會出現幻讀問題呢?

Java的內置鎖以對象為單位,RDBMS的鎖呢?前面的注釋中略有介紹。為了提高并發性能,簡單的以數據表、數據庫為單位實現鎖的性能過低;標準SQL中,讀、寫鎖以記錄(數據行)為單位,范圍鎖以范圍(邏輯上的范圍,用where描述)為單位。如果沒有范圍鎖,那么顯然讀、寫鎖只能“鎖”在已存在的記錄上。假設操作序列,這次具體一些:

  1. 事務1開始
  2. 事務1統計表內數據的總行數
  3. 事務2開始
  4. 事務2插入一條新紀錄
  5. 事務2提交
  6. 事務1利用“舊的總行數+新的數據表內容”計算區分度
  7. 事務1提交

該操作序列是讀已提交中操作序列的一個具體實例。因此,可以解決部分不可重復讀問題,不能解決的那部分就是幻讀了。

以基于鎖的“串行化”方案為例(“多版本+并發沖突”同理),假設不使用范圍鎖,則幻讀表現如下:由于事務2插入的記錄不獲取鎖,操作2獲取的讀鎖無法發揮作用,操作5提交事務2后,新記錄就對事務1可見了;操作6讀取時,事務1認為一致性依然滿足,便使用了舊的總行數,并重新讀表計算distinct count,卻讀到了一條意料之外的新紀錄,破壞了一致性——好像出現了幻覺一樣,這條新紀錄就被稱為“幻行”,該現象即“幻讀”。

可序列化

對于基于鎖的“串行化”方案,可序列化實現:從各操作開始前持有讀鎖、寫鎖、范圍鎖,直到事務提交后釋放。對于“多版本 + 沖突檢測”方案,可序列化基于更嚴格的寫沖突檢測來實現,詳見“快照隔離”技術,此處不展開。

范圍鎖如何解決幻讀問題呢?

范圍鎖是一個邏輯概念上的鎖,事務從讀、寫操作(帶顯式或隱式where)開始前持有范圍鎖,直到事務提交后釋放。忽略讀、寫鎖,對可重復讀中操作序列的影響如下:操作2中事務1獲取了目標范圍上的范圍鎖,操作4發現目標范圍被鎖,陷入阻塞,直到操作7事務提交。

隔離性級別的總結

各隔離級別解決了不同的問題。"Y"說明存在問題,"-"說明不存在:

隔離級別/問題 臟讀 不可重復讀 幻讀
讀未提交 Y Y Y
讀已提交 - Y Y
可重復讀 - - Y
可序列化 - - -

在基于鎖的并發控制中,依靠不同的鎖持有時間實現各隔離級別。鎖均從操作前開始持有,"S"表示操作結束后釋放,"C"表示事務提交后釋放:

隔離級別/問題 臟讀 不可重復讀 幻讀
讀未提交 S S S
讀已提交 C S S
可重復讀 C C S
可序列化 C C C

MySQL的默認隔離級別是可重復讀,解決了臟讀、部分不可重復讀問題,有幻讀問題。


參考:


本文鏈接:事務的ACID和四個隔離級別
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協議發布,歡迎轉載,演繹或用于商業目的,但是必須保留本文的署名及鏈接。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容