事務特性
-
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 隔離級別下是如何實現讀不阻塞的? 呵呵一笑百媚生的答案
可見性判斷
假設要讀取的行的最後提交事務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,說明該記錄未被刪除,可以返回,否則不返回。
RR和RC隔離級別
Repeatable Read和Read Committed隔離級別都是基於read view來實現,不同之處在於:
-
Repeatable Read
read view是在執行事務中第一條select語句的瞬間建立,後續所有的select都是複用這個物件,所以能保證每次讀取的一致性。(可重複讀的語義)
-
Read Committed
事務中每條select語句都會建立read view,這樣就可以讀取到其它事務已經提交的內容。
對於InnoDB來說,Repeatable Read雖然比Read Committed隔離級別高,開銷反而相對較小。