事務的ACID和四個隔離級別

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

ACID

事務指"一個被視爲單一的工做單元的操做序列"。一個良好的事務處理系統,必須具有四個標準特性,即**ACID**:git

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

隔離性的4個級別

在理解隔離性級別時,很容易混淆「幻讀」與「不可重複讀」的問題。這裏先對4個隔離性級別給出概覽;而後分析原理,從實現角度理解各類問題;最後做出總結。github

概覽

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

讀未提交

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

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

讀已提交

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

狹義上,讀已提交解決了髒讀的問題。這個級別有時候叫作不可重複讀,面臨不可重複讀的問題:兩次執行一樣的查詢,若是第二次讀到了其餘事務提交的結果,則會獲得不同的結果優化

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

可重複讀

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

未能解決的那部分稱爲幻讀當某個事務在讀取目標範圍內的記錄時,另外一個事務又在該範圍內插入了新的記錄,當以前的事務再次讀取該範圍的記錄時,會產生第一次讀取範圍時不存在的**幻行**(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提交釋放鎖。

「串行化」不一樣於「可序列化」。爲了區分,前、後文中均將隔離性級別稱爲「可序列化」,將此處的悲觀策略稱爲「串行化」。

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

「多版本 + 衝突檢測」是更常見的實現方案:多個事務採用多個版本,最後提交時檢測是否與當前數據版本衝突,若是衝突則報錯提醒,不然成功提交

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

對於讀已提交中的操做序列,事務一、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 出處:monkeysayhi.github.io 本文基於 知識共享署名-相同方式共享 4.0 國際許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名及連接。

相關文章
相關標籤/搜索