數據庫事務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 COMMITTED 和 REPEATABLE 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的可見性判斷規則
訪問記錄的某個版本時,通過以下步驟判斷是否可見:
- 若版本的trx_id == creator_trx_id:當前事務訪問自己修改的記錄,可見。
- 若版本的trx_id < min_trx_id:生成該版本的事務在當前ReadView生成前已提交,可見。
- 若版本的trx_id >= max_trx_id:生成該版本的事務在當前ReadView生成後才開啓,不可見。
- 若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(可避免髒讀,允許不可重複讀和幻讀)。