資料庫事務和MVCC多版本併發控制
- 髒讀
一個事務正在對一條記錄進行修改,在這個事務完成並提交前,這條記錄的資料就處於不一致狀態。這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“髒”資料,並據此做進一步的處理,就會產生未提交的資料依賴關係。
時間 | 事務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_tlow_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_tup_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 隔離級別下是如何實現讀不阻塞的? 呵呵一笑百媚生的答案

可見性判斷
假設要讀取的行的最後提交事務id(即當前資料行的穩定事務id)為 trx_id,可見性比較過程如下:
- trx_id < up_limit_id => 此記錄的最後一次修改在read view建立之前,跳轉到步驟5;
- trx_id > low_limit_id => 此記錄的最後一次修改在read view建立之後,跳轉到步驟4;
- up_limit_id <= trx_id <= low_limit_id => 從up_limit_id到low_limit_id進行遍歷,如果trx_id等於他們之中的某個事務id的話,表示該記錄的最後一次修改尚未儲存,跳轉到步驟4。否則跳轉到步驟5;
- 從此記錄的DB_ROLL_PTR指標所指向的undo log(此記錄的上一次修改),將undo log的DB_TRX_ID賦值給trx_id,跳轉到步驟1重新開始計算可見性;
- 如果此記錄的DELELE_BIT為false,說明該記錄未被刪除,可以返回,否則不返回。