事務(wù)特性
- Atomicity(原子性)
一個事務(wù)必須被視為一個不可分割的最小工作單位,整個事務(wù)中的所有操作要么全部提交成功,要么全部失敗回滾。 - Consistency(一致性)
數(shù)據(jù)庫總是從一個一致性狀態(tài)轉(zhuǎn)換到另一個一致性狀態(tài),事務(wù)執(zhí)行之前和執(zhí)行之后都必須處于一致性狀態(tài)。 - Isolation(隔離性)
通常來說,一個事務(wù)所做的修改在最終提交之前,對其它事務(wù)是不可見的。關(guān)于事務(wù)的隔離性,數(shù)據(jù)庫提供了多種隔離級別。 - Durability(持久性)
一旦事務(wù)提交,則其所做的修改就會永久保存到數(shù)據(jù)庫中。即便是數(shù)據(jù)庫系統(tǒng)遇到故障的情況下也不會丟失。
并發(fā)事務(wù)的問題
- 臟讀
一個事務(wù)正在對一條記錄進行修改,在這個事務(wù)完成并提交前, 這條記錄的數(shù)據(jù)就處于不一致狀態(tài)。這時, 另一個事務(wù)也來讀取同一條記錄,如果不加控制,第二個事務(wù)讀取了這些“臟”數(shù)據(jù),并據(jù)此做進一步的處理,就會產(chǎn)生未提交的數(shù)據(jù)依賴關(guān)系。時間 事務(wù)A 事務(wù)B T1 開啟事務(wù) 開啟事務(wù) T2 查詢賬戶余額為1000 T3 充值500,余額修改為1500 T4 查詢余額為1500 T5 撤銷事務(wù),余額改回1000 T6 匯入500,余額修改為2000 T7 提交事務(wù) - 不可重復(fù)讀
一個事務(wù)在讀取某些數(shù)據(jù)后的某個時間,再次讀取以前讀過的數(shù)據(jù),卻發(fā)現(xiàn)其讀出的數(shù)據(jù)已經(jīng)發(fā)生了變更、或者某些記錄已經(jīng)被刪除了。時間 事務(wù)A 事務(wù)B T1 開啟事務(wù) 開啟事務(wù) T2 select * from user where user_id=100 假設(shè)為小明的用戶信息 T3 將user_id=100的用戶信息對應(yīng)的年齡修改為18 T4 提交事務(wù) T5 再次查詢發(fā)現(xiàn)用戶的年齡變更了 T6 ... T7 提交事務(wù) - 幻讀
一個事務(wù)按相同的查詢條件重新讀取以前檢索過的數(shù)據(jù),卻發(fā)現(xiàn)其它事務(wù)插入了滿足其查詢條件的新數(shù)據(jù)。時間 事務(wù)A 事務(wù)B T1 開啟事務(wù) 開啟事務(wù) T2 select * from user where age=18 假設(shè)得到兩條記錄 T3 向user表插入一條age=18的新記錄 T4 提交事務(wù) T5 再次查詢得到三條記錄 T6 .. T7 提交事務(wù)
幻讀和不可重復(fù)讀的區(qū)別
- 不可重復(fù)讀的重點是修改:在同一事務(wù)中,相同的條件,第一次和第二次讀到的數(shù)據(jù)不一致(中間有其它事務(wù)提交了修改)。
- 幻讀的重點是新增或者刪除:在同一事務(wù)中,相同的條件,第一次和第二次讀到的記錄數(shù)不一樣(中間有其它事務(wù)提交了新增或者刪除)。
事務(wù)隔離級別
SQL標(biāo)準(zhǔn)定義了4類隔離級別,每一種級別都規(guī)定了一個事務(wù)中所做的修改,哪些在事務(wù)內(nèi)和事務(wù)間是可見的,哪些是不可見的。
Read Uncommited
所有事務(wù)都可以看到其它未提交事務(wù)的執(zhí)行結(jié)果,該隔離級別一般不會使用。Read Committed(RC)
一個事務(wù)只能看到已經(jīng)提交的事務(wù)所做的變更。Repeatable Read(RR)
確保同一事務(wù)的多個實例在并發(fā)讀取數(shù)據(jù)時會看到相同的數(shù)據(jù)行。-
Serializable
完全串行化讀,每次讀都需要獲得表級共享鎖,讀寫相互阻塞。
隔離級別 臟讀 不可重復(fù)讀 幻讀 Read Uncommited Yes Yes Yes Read Committed No Yes Yes Repeatable Read No No Yes Serializable No No No 并發(fā)事務(wù)解決方案
臟讀、不可重復(fù)讀和幻讀都是數(shù)據(jù)庫讀一致性問題,需要由數(shù)據(jù)庫提供一定的事務(wù)隔離機制來解決。
(1)鎖機制
解決寫-寫沖突問題。在讀取數(shù)據(jù)前,對其加鎖,防止其它事務(wù)對該數(shù)據(jù)進行修改。
-
悲觀鎖
往往依靠數(shù)據(jù)庫提供的鎖機制。
-
樂觀鎖
大多是基于數(shù)據(jù)版本記錄機制來實現(xiàn)。
(2)MVCC多版本并發(fā)控制
解決讀-寫沖突問題。不用加鎖,通過一定機制生成一個數(shù)據(jù)請求時間點時的一致性數(shù)據(jù)快照, 并用這個快照來提供一定級別 (語句級或事務(wù)級) 的一致性讀取。這樣在讀操作的時候不需要阻塞寫操作,寫操作時不需要阻塞讀操作。
MVCC多版本并發(fā)控制
Mysql的大多數(shù)事務(wù)型存儲引擎實現(xiàn)都不是簡單的行級鎖,基于并發(fā)性能考慮,一般都實現(xiàn)了MVCC多版本并發(fā)控制。MVCC是通過保存數(shù)據(jù)在某個時間點的快照來實現(xiàn)的。不管事務(wù)執(zhí)行多長時間,事務(wù)看到的數(shù)據(jù)都是一致的。
讀操作
讀操作分成兩類:快照讀和當(dāng)前讀。
快照讀:簡單的select操作屬于快照讀,不加鎖。
- select * from table where ? ;
當(dāng)前讀:特殊的讀操作,插入/更新/刪除操作,屬于當(dāng)前讀,需要加鎖。
select * from table where ? lock in share mode ;
select * from table where ? for update ;
update table set ? where ? ;
delete from table where ? ;
數(shù)據(jù)存儲
innodb存儲引擎中,每行數(shù)據(jù)都包含了一些隱藏字段:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR和DELETE_BIT。
-

- DB_TRX_ID:用來標(biāo)識最近一次對本行記錄做修改的事務(wù)的標(biāo)識符,即最后一次修改本行記錄的事務(wù)id。delete操作在內(nèi)部來看是一次update操作,更新行中的刪除標(biāo)識位DELELE_BIT。
- DB_ROLL_PTR:指向當(dāng)前數(shù)據(jù)的undo log記錄,回滾數(shù)據(jù)通過這個指針來尋找記錄被更新之前的內(nèi)容信息。
- DB_ROW_ID:包含一個隨著新行插入而單調(diào)遞增的行ID, 當(dāng)由innodb自動產(chǎn)生聚集索引時,聚集索引會包括這個行ID的值,否則這個行ID不會出現(xiàn)在任何索引中。
- DELELE_BIT:用于標(biāo)識該記錄是否被刪除。
數(shù)據(jù)操作
- insert
創(chuàng)建一條記錄,DB_TRX_ID為當(dāng)前事務(wù)ID,DB_ROLL_PTR為NULL。 - delete
將當(dāng)前行的DB_TRX_ID設(shè)置為當(dāng)前事務(wù)ID,DELELE_BIT設(shè)置為1。 - update 復(fù)制一行,新行的DB_TRX_ID為當(dāng)前事務(wù)ID,DB_ROLL_PTR指向上個版本的記錄,事務(wù)提交后DB_ROLL_PTR設(shè)置為NULL。
- select
1、只查找創(chuàng)建早于當(dāng)前事務(wù)ID的記錄,確保當(dāng)前事務(wù)讀取到的行都是事務(wù)之前就已經(jīng)存在的,或者是由當(dāng)前事務(wù)創(chuàng)建或修改的;
2、行的DELETE BIT為1時,查找刪除晚于當(dāng)前事務(wù)ID的記錄,確保當(dāng)前事務(wù)開始之前,行沒有被刪除。
一致性讀
Mysql的一致性讀是通過read view結(jié)構(gòu)來實現(xiàn)。
read view主要是用來做可見性判斷的,它維護的是本事務(wù)不可見的當(dāng)前其他活躍事務(wù)。其中最早的事務(wù)ID為up_limit_id,最遲的事務(wù)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應(yīng)該是當(dāng)前系統(tǒng)尚未分配的下一個事務(wù)ID,也就是目前已經(jīng)出現(xiàn)過的事務(wù)ID的最大值+1。

可見性判斷
假設(shè)要讀取的行的最后提交事務(wù)id(即當(dāng)前數(shù)據(jù)行的穩(wěn)定事務(wù)id)為 trx_id,可見性比較過程如下:
- trx_id < up_limit_id => 此記錄的最后一次修改在read view創(chuàng)建之前,跳轉(zhuǎn)到步驟5;
- trx_id > low_limit_id => 此記錄的最后一次修改在read view創(chuàng)建之后,跳轉(zhuǎn)到步驟4;
- up_limit_id <= trx_id <= low_limit_id => 從up_limit_id到low_limit_id進行遍歷,如果trx_id等于他們之中的某個事務(wù)id的話,表示該記錄的最后一次修改尚未保存,跳轉(zhuǎn)到步驟4。否則跳轉(zhuǎn)到步驟5;
- 從此記錄的DB_ROLL_PTR指針?biāo)赶虻膗ndo log(此記錄的上一次修改),將undo log的DB_TRX_ID賦值給trx_id,跳轉(zhuǎn)到步驟1重新開始計算可見性;
- 如果此記錄的DELELE_BIT為false,說明該記錄未被刪除,可以返回,否則不返回。
RR和RC隔離級別
Repeatable Read和Read Committed隔離級別都是基于read view來實現(xiàn),不同之處在于:
- Repeatable Read
read view是在執(zhí)行事務(wù)中第一條select語句的瞬間創(chuàng)建,后續(xù)所有的select都是復(fù)用這個對象,所以能保證每次讀取的一致性。(可重復(fù)讀的語義) - Read Committed
事務(wù)中每條select語句都會創(chuàng)建read view,這樣就可以讀取到其它事務(wù)已經(jīng)提交的內(nèi)容。
對于InnoDB來說,Repeatable Read雖然比Read Committed隔離級別高,開銷反而相對較小。
參考資料
數(shù)據(jù)庫事務(wù)與MySQL事務(wù)總結(jié)
MySQL-InnoDB-MVCC多版本并發(fā)控制
MySQL InnoDB MVCC深度分析
樂觀鎖和 MVCC 的區(qū)別?
MySQL 在 RC 隔離級別下是如何實現(xiàn)讀不阻塞的?