數據庫事務ACID特性與隔離級別

數據庫事務ACID特性

數據庫事務正確執行的四個基礎要素是原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)。

  • 原子性:是指事務包含的所有操作要麼全部成功,要麼全部失敗回滾,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有被執行過一樣。
  • 一致性:是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是説一個事務執行之前和執行之後都必須處於一致性狀態。拿轉賬來説,假設用户A和用户B兩者的錢加起來一共是5000,那麼不管A和B之間如何轉賬,轉幾次賬,事務結束後兩個用户的錢相加起來應該還得是5000,這就是事務的一致性。
  • 隔離性:兩個事務之間的隔離程度
  • 持久性:持久性是指一個事務一旦被提交了,那麼對數據庫中的數據的改變就是永久性的,即便是在數據庫系統遇到故障的情況下(如斷電、崩潰)也不會丟失提交事務的操作。

丟失更新

在互聯網中存在着搶購、秒殺等高併發環境,使得數據庫在一個多事務的環境中運行,多個事務的併發會產生一系列的問題,主要的問題之一就是丟失更新,一般而言存在兩類丟失更新。

假設一個場景,一個賬户存在互聯網消費和刷卡消費兩種形式,而一對夫妻共用這個賬户。男喜歡刷卡消費,女喜歡互聯網消費,那麼可能產生如下表所示場景

表1 第一類丟失更新

時間

事務一(男)

事務二(女)

T1

查詢賬户餘額為10000元

T2

查詢賬户餘額為10000元

T3

網購1000元

T4

請客吃飯消費1000元

T5

提交事務成功,餘額9000元

T6

取消購買,回滾事務到T2時刻,餘額10000元

整個過程只有男消費1000元,而在最後的T6時刻,女回滾事務,卻恢復了原來的初始值10000元,這顯然不符合事實。這樣的兩個事務併發,一個回滾、一個提交成功導致不一致,稱之為第一類丟失更新。大部分數據庫(包括Mysql和Oracle)基本都已經消滅了這類丟失更新。第二類丟失更新是我們真正需要關注的內容。

表2 第二類丟失更新

時間

事務一(男)

事務二(女)

T1

查詢賬户餘額為10000元

T2

查詢賬户餘額為10000元

T3

網購1000元

T4

請客吃飯消費1000元

T5

提交事務成功,餘額9000元

T6

提交事務,根據之前餘額10000元,扣減1000元后,餘額為9000元

整個過程存在兩筆交易,一筆是男的請客吃飯,一筆是女的網購,兩者都提交了事務,由於在不同的事務中,無法探知其它事務的操作,導致兩者提交後,餘額都為9000元,而實際正確的應為8000元,這就是第二類丟失更新。為了克服事務之間協助的一致性,數據庫標準規範中定義了事務之間的隔離級別,來在不同程度上減少出現丟失更新的可能性---->數據庫隔離級別。

隔離級別

隔離級別可以在不同程度上減少丟失更新,按照SQL的標準規範,把隔離級別定義為4層,分別是:髒讀(dirty read)、讀/寫提交(read commit)、可重複讀(repeatable read)和序列化(serializable)。

各類隔離級別和產生的現象

隔離級別

髒讀

不可重複讀

幻讀

髒讀(Read Uncommitted)




讀/寫提交(Read Committed)

×



可重複讀(Repeatable Read)

×

×


序列化(Serializable)

×

×

×

√表示該隔離級別下會出現對應問題,× 表示不會出現。

髒讀是最低的隔離級別,允許一個事務去讀取另一個事務中未提交的數據。

髒讀

時間

事務一(男)

事務二(女)

備註

T1

查詢餘額10000元

T2

查詢餘額10000元

T3

網購1000元,餘額9000元

T4

請客吃飯消費1000元,餘額8000元

讀取到事務二,未提交餘額為9000元,所以餘額為8000元

T5

提交事務

餘額為8000元

T6

回滾事務

由於第一類丟失更新已經克服,所以餘額為錯誤的8000元

由於在T3時刻女啓動了消費,導致餘額為9000元,男在T4時刻消費,因為用了髒讀,所以能夠讀取女消費的餘額(事務二未提交的)為9000元,這樣餘額就為8000元了,於是T5時刻男提價事務,餘額變為了8000元,女在T6時刻回滾事務,由於第一類丟失更新已經克服,所以餘額為錯誤的8000元,顯然這是一個錯誤的餘額,產生這個錯誤的根源來自於T4時刻,也就是事務一讀取到事務二未提交的事務,這樣的場景稱之為髒讀。

為了克服髒讀,SQL標準提出了第二個隔離級別----讀/寫提交。所謂讀寫提交,就是説一個事務只能讀取另一個事務已經提交的數據。

讀/寫提交

時間

事務一(男)

事務二(女)

備註

T1

查詢餘額10000元

T2

查詢餘額10000元

T3

網購1000元,餘額9000元

T4

請客吃飯消費1000元,餘額9000元

由於事務二的餘額未提交,採取讀/寫提交時不能讀出,所以餘額為9000元

T5

提交事務

餘額為9000元

T6

回滾事務

由於第一類丟失更新已經克服,所以餘額依舊為正確的9000元

在T3時刻由於事務採取讀/寫提交的隔離級別,所以男無法讀取女未提交的9000元餘額,他只能讀取到10000元,所以在消費後餘額依舊為9000元。T5時刻提交事務,而T6時刻女回滾事務,所以結果為正確的9000元,這樣就消除了髒讀帶來的問題,但是也會引發其它問題,如下表所示。

不可重複讀

時間

事務一(男)

事務二(女)

備註

T1

查詢餘額10000元

T2

查詢餘額10000元

T3

網購1000元,餘額9000元

T4

請客吃飯消費2000元,餘額8000元

由於採取讀/寫提交,不能讀取事務二中未提交的餘額9000元

T5

繼續購物8000元,餘額1000元

由於採取讀/寫提交,不能讀取事務一中未提交的餘額8000元

T6

提交事務,餘額1000元

女提交事務,餘額更新為1000元

T7

提交事務發現餘額為1000元,不足以買單

由於採取讀/寫提交,因此此時事務一可以知道餘額不足

由於T7時刻事務一知道事務二提交的結果----餘額為1000元,導致男無錢買單的尷尬。對於男而言,他並不知道女做了什麼事情,但是賬户餘額卻莫名其妙地從10000元變為了1000元,對他來説,賬户餘額是不能重複讀取的,而是一個會變化的值,這樣的場景稱之為不可重複讀(unrepeatable read),這是讀/寫提交存在的問題。

為了克服不可重複讀帶來的錯誤,SQL標準又提出了一個可重複讀的隔離級別來解決問題。注意,可重複讀針對的是數據庫同一條記錄而言的,換句話説,可重複讀會使得同一條記錄的讀/寫按照一個序列化進行操作,不會產生交叉情況,這樣就能保證同一條數據的一致性,進而保證上述場景的正確性。但是由於數據庫並不是只能針對一條數據進行讀/寫操作,在很多場景,數據庫需要同時對多條記錄進行讀/寫,這個時候會產生下面的情況。如下表所示

幻讀

時間

事務一(男)

事務二(女)

備註

T1

查詢消費記錄為10條,準備打印

初始狀態

T2

啓用消費一筆

T3

提價事務

T4

打印消費記錄得到11條

女發現打印了11條消費記錄,比查詢的10條多了一條。她會認為這條是多餘不存在的,這樣的場景稱之為幻讀。

女在T1查詢得到10條記錄,到T4打印記錄時,並不知道男在T2和T3時刻進行了消費,導致多一條(可重複讀針對的是同一條記錄,而這裏不是同一條記錄)消費記錄的產生,她會質疑這條多出來的記錄是不是幻讀出來的,這樣的場景稱之為幻讀。

為了克服幻讀,SQL標準又提出了序列化的隔離級別。它是一種讓SQL按照順序讀/寫的方式,能夠消除數據庫事務之間併發產生數據不一致的問題。

MySQL的事務和MVCC

MySQL事務支持情況

MySQL中並非所有存儲引擎都支持事務:

  • 支持事務:InnoDB、NDB
  • 不支持事務:MyISAM(常用但無事務特性)

事務ACID的實現機制

  • 原子性:通過 undo_log日誌 實現(事務失敗時回滾到初始狀態)。
  • 持久性:通過 redo_log重做日誌 實現(事務提交後,數據修改持久化到磁盤)。
  • 隔離性:通過 鎖機制 + MVCC 共同實現(保證併發事務的隔離性)。
  • 一致性:事務的最終目標,由原子性、隔離性、持久性共同保障。

InnoDB與鎖配合,同時採用另一種事務隔離性的實現機制MVCC,即 Multi-Versioned Concurrency Control 多版本併發控制,用來解決髒讀、不可重複讀等事務之間讀寫問題,MVCC在某些場景中替代了低效的鎖,在保證了隔離性的基礎上,提升了讀取效率和併發性。

MVCC 多版本併發控制

定義

MVCC(Multi-Versioned Concurrency Control)即多版本併發控制,是InnoDB存儲引擎的核心特性之一。
作用:在 READ COMMITTEDREPEATABLE READ 隔離級別下,事務執行普通SELECT操作時,通過訪問記錄的“版本鏈”實現讀寫併發,替代部分低效鎖機制,提升讀取效率和併發性能。

版本鏈的構成

InnoDB的聚簇索引記錄包含兩個必要隱藏列和一個非必要隱藏列:

  • trx_id:事務id。每次事務修改該記錄時,會將當前事務id賦值給此列。
  • roll_pointer:回滾指針。每次修改記錄時,會將舊版本記錄寫入undo日誌,此列作為指針指向該舊版本記錄。
  • row_id(非必要):當創建的表中有主鍵或者非NULL的UNIQUE鍵時都不會包含row_id列。

版本鍊形成過程

每次對記錄進行INSERT/UPDATE/DELETE操作時,都會生成一條undo日誌,undo日誌通過roll_pointer屬性串聯成鏈表,形成“版本鏈”(最新版本在鏈表頭部,舊版本依次向後)。


ReadView的作用與構成

核心問題

當事務使用READ COMMITTED或REPEATABLE READ隔離級別時,普通SELECT查詢需判斷版本鏈中的哪個版本對當前事務“可見”(即是否能讀取)。

ReadView的定義

對於使用READ COMMITTED和REPEATABLE READ隔離級別的事務來説,都必須保證讀到已經提交了的事務修改過的記錄,也就是説假如另一個事務已經修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的。因此,核心問題就是需要判斷一下版本鏈中的哪個版本是當前事務可見的。包含4個關鍵屬性:

  • m_ids:生成ReadView時,當前系統中活躍的讀寫事務id列表。
  • min_trx_id:m_ids中的最小事務id(當前活躍事務的最小id)。
  • max_trx_id:生成ReadView時,系統即將分配給下一個事務的id(活躍事務最大id + 1)。
  • creator_trx_id:創建當前ReadView的事務id(當前事務自身的id)。


ReadView的可見性判斷規則

訪問記錄的某個版本時,通過以下步驟判斷是否可見:

  1. 若版本的trx_id == creator_trx_id:當前事務訪問自己修改的記錄,可見
  2. 若版本的trx_id < min_trx_id:生成該版本的事務在當前ReadView生成前已提交,可見
  3. 若版本的trx_id >= max_trx_id:生成該版本的事務在當前ReadView生成後才開啓,不可見
  4. 若min_trx_id ≤ trx_id < max_trx_id:
  • 若trx_id在m_ids列表中:生成該版本的事務仍活躍,不可見
  • 若trx_id不在m_ids列表中:生成該版本的事務已提交,可見


ReadView的生成時機(隔離級別核心區別)

READ COMMITTED和REPEATABLE READ的核心差異在於生成ReadView的時機:

  • READ COMMITTED:每次執行SELECT查詢前,都會重新生成一個ReadView。
  • 後果:同一事務內多次查詢可能讀取到不同版本的數據(其他事務提交後的修改),因此會出現不可重複讀。
  • REPEATABLE READ:僅在事務第一次執行SELECT查詢時生成一個ReadView,後續查詢複用該ReadView。
  • 後果:同一事務內多次查詢讀取到的是同一版本的數據,避免了不可重複讀,但仍可能出現幻讀。

MySQL與Oracle的默認隔離級別

  • MySQL默認隔離級別:REPEATABLE READ(可避免髒讀、不可重複讀,部分防止幻讀,但無法完全消除)。
  • Oracle默認隔離級別:READ COMMITTED(可避免髒讀,允許不可重複讀和幻讀)。