資料庫事務和MVCC多版本併發控制

夢之痕發表於2019-02-03

事務特性

  • Atomicity(原子性)

    一個事務必須被視為一個不可分割的最小工作單位,整個事務中的所有操作要麼全部提交成功,要麼全部失敗回滾。

  • Consistency(一致性)

    資料庫總是從一個一致性狀態轉換到另一個一致性狀態,事務執行之前和執行之後都必須處於一致性狀態。

  • Isolation(隔離性)

    通常來說,一個事務所做的修改在最終提交之前,對其它事務是不可見的。關於事務的隔離性,資料庫提供了多種隔離級別。

  • Durability(永續性)

    一旦事務提交,則其所做的修改就會永久儲存到資料庫中。即便是資料庫系統遇到故障的情況下也不會丟失。

併發事務的問題

  • 髒讀

一個事務正在對一條記錄進行修改,在這個事務完成並提交前,這條記錄的資料就處於不一致狀態。這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“髒”資料,並據此做進一步的處理,就會產生未提交的資料依賴關係。

時間 事務A 事務B
T1 開啟事務 開啟事務
T2 查詢賬戶餘額為1000
T3 充值500,餘額修改為1500
T4 查詢餘額為1500
T5 撤銷事務,餘額改回1000
T6 匯入500,餘額修改為2000
T7 提交事務
  • 不可重複讀

一個事務在讀取某些資料後的某個時間,再次讀取以前讀過的資料,卻發現其讀出的資料已經發生了變更、或者某些記錄已經被刪除了。

時間 事務A 事務B
T1 開啟事務 開啟事務
T2 select * from user where user_id=100 假設為小明的使用者資訊
T3 將user_id=100的使用者資訊對應的年齡修改為18
T4 提交事務
T5 再次查詢發現使用者的年齡變更
T6 ...
T7 提交事務
  • 幻讀

一個事務按相同的查詢條件重新讀取以前檢索過的資料,卻發現其它事務插入了滿足其查詢條件的新資料。

時間 事務A 事務B
T1 開啟事務 開啟事務
T2 select * from user where age=18 假設得到兩條記錄
T3 向user表插入一條age=18的新記錄
T4 提交事務
T5 再次查詢得到三條記錄
T6 ..
T7 提交事務

幻讀和不可重複讀的區別

  • 不可重複讀的重點是修改:在同一事務中,相同的條件,第一次和第二次讀到的資料不一致(中間有其它事務提交了修改)。
  • 幻讀的重點是新增或者刪除:在同一事務中,相同的條件,第一次和第二次讀到的記錄數不一樣(中間有其它事務提交了新增或者刪除)。

事務隔離級別

SQL標準定義了4類隔離級別,每一種級別都規定了一個事務中所做的修改,哪些在事務內和事務間是可見的,哪些是不可見的。

  • Read Uncommited

所有事務都可以看到其它未提交事務的執行結果,該隔離級別一般不會使用。

  • Read Committed(RC)

一個事務只能看到已經提交的事務所做的變更。

  • Repeatable Read(RR)

確保同一事務的多個例項在併發讀取資料時會看到相同的資料行。

  • Serializable

完全序列化讀,每次讀都需要獲得表級共享鎖,讀寫相互阻塞。

隔離級別 髒讀 不可重複讀 幻讀
Read Uncommited Yes Yes Yes
Read Committed No Yes Yes
Repeatable Read No No Yes
Serializable No No No

併發事務解決方案

髒讀、不可重複讀和幻讀都是資料庫讀一致性問題,需要由資料庫提供一定的事務隔離機制來解決。

(1)鎖機制

解決寫-寫衝突問題。在讀取資料前,對其加鎖,防止其它事務對該資料進行修改。

  • 悲觀鎖

    往往依靠資料庫提供的鎖機制。

  • 樂觀鎖

    大多是基於資料版本記錄機制來實現。

(2)MVCC多版本併發控制

解決讀-寫衝突問題。不用加鎖,通過一定機制生成一個資料請求時間點時的一致性資料快照, 並用這個快照來提供一定級別 (語句級或事務級) 的一致性讀取。這樣在讀操作的時候不需要阻塞寫操作,寫操作時不需要阻塞讀操作。

MVCC多版本併發控制

Mysql的大多數事務型儲存引擎實現都不是簡單的行級鎖,基於併發效能考慮,一般都實現了MVCC多版本併發控制。MVCC是通過儲存資料在某個時間點的快照來實現的。不管事務執行多長時間,事務看到的資料都是一致的。

讀操作

讀操作分成兩類:快照讀和當前讀。

快照讀:簡單的select操作屬於快照讀,不加鎖。

  • select * from table where ? ;

當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,需要加鎖。

  • select * from table where ? lock in share mode ;
  • select * from table where ? for update ;
  • update table set ? where ? ;
  • delete from table where ? ;

資料儲存

innodb儲存引擎中,每行資料都包含了一些隱藏欄位:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR和DELETE_BIT。

資料庫儲存結構

  • DB_TRX_ID:用來標識最近一次對本行記錄做修改的事務的識別符號,即最後一次修改本行記錄的事務id。delete操作在內部來看是一次update操作,更新行中的刪除標識位DELELE_BIT。
  • DB_ROLL_PTR:指向當前資料的undo log記錄,回滾資料通過這個指標來尋找記錄被更新之前的內容資訊。
  • DB_ROW_ID:包含一個隨著新行插入而單調遞增的行ID, 當由innodb自動產生聚集索引時,聚集索引會包括這個行ID的值,否則這個行ID不會出現在任何索引中。
  • DELELE_BIT:用於標識該記錄是否被刪除。

資料操作

  • insert

    建立一條記錄,DB_TRX_ID為當前事務ID,DB_ROLL_PTR為NULL。

  • delete

    將當前行的DB_TRX_ID設定為當前事務ID,DELELE_BIT設定為1。

  • update 複製一行,新行的DB_TRX_ID為當前事務ID,DB_ROLL_PTR指向上個版本的記錄,事務提交後DB_ROLL_PTR設定為NULL。

  • select

    1、只查詢建立早於當前事務ID的記錄,確保當前事務讀取到的行都是事務之前就已經存在的,或者是由當前事務建立或修改的;

    2、行的DELETE BIT為1時,查詢刪除晚於當前事務ID的記錄,確保當前事務開始之前,行沒有被刪除。

一致性讀

Mysql的一致性讀是通過read view結構來實現。 read view主要是用來做可見性判斷的,它維護的是本事務不可見的當前其他活躍事務。其中最早的事務ID為up_limit_id,最遲的事務ID為low_limit_id

	trx_id_t	low_limit_id;
				/*!< The read should not see any transaction
				with trx id >= this value. In other words,
				this is the "high water mark". */
	trx_id_t	up_limit_id;
				/*!< The read should see all trx ids which
				are strictly smaller (<) than this value.
				In other words,
				this is the "low water mark". */
複製程式碼

如何理解low_limit_id

可以參考知乎這個答案來理解。low_limit_id應該是當前系統尚未分配的下一個事務ID(從這個語義來更容易理解),也就是目前已經出現過的事務ID的最大值+1

MySQL 在 RC 隔離級別下是如何實現讀不阻塞的? 呵呵一笑百媚生的答案

low_limit_id理解

可見性判斷

假設要讀取的行的最後提交事務id(即當前資料行的穩定事務id)為 trx_id,可見性比較過程如下:

  1. trx_id < up_limit_id => 此記錄的最後一次修改在read view建立之前,跳轉到步驟5;
  2. trx_id > low_limit_id => 此記錄的最後一次修改在read view建立之後,跳轉到步驟4;
  3. up_limit_id <= trx_id <= low_limit_id => 從up_limit_id到low_limit_id進行遍歷,如果trx_id等於他們之中的某個事務id的話,表示該記錄的最後一次修改尚未儲存,跳轉到步驟4。否則跳轉到步驟5;
  4. 從此記錄的DB_ROLL_PTR指標所指向的undo log(此記錄的上一次修改),將undo log的DB_TRX_ID賦值給trx_id,跳轉到步驟1重新開始計算可見性;
  5. 如果此記錄的DELELE_BIT為false,說明該記錄未被刪除,可以返回,否則不返回。

RR和RC隔離級別

Repeatable Read和Read Committed隔離級別都是基於read view來實現,不同之處在於:

  • Repeatable Read

    read view是在執行事務中第一條select語句的瞬間建立,後續所有的select都是複用這個物件,所以能保證每次讀取的一致性。(可重複讀的語義

  • Read Committed

    事務中每條select語句都會建立read view,這樣就可以讀取到其它事務已經提交的內容。

對於InnoDB來說,Repeatable Read雖然比Read Committed隔離級別高,開銷反而相對較小。

參考資料

資料庫事務與MySQL事務總結

MySQL-InnoDB-MVCC多版本併發控制

MySQL InnoDB MVCC深度分析

樂觀鎖和 MVCC 的區別?

MySQL 在 RC 隔離級別下是如何實現讀不阻塞的?

相關文章