事務隔離級別
讀未提交(Read Uncommitted)
允許事務讀取其他事務未提交的數據,可能會導致臟讀。
讀已提交(Read Committed)
一個事務只能看見已經提交的事務所做的更改,可以避免臟讀,但可能會遇到不可重復讀。
可重復讀(Repeatable Read)
在一個事務內,多次讀取同一數據的結果是一致的,即使其他事務在這期間對數據進行了修改和提交。此隔離級別可以防止不可重復讀,但可能遇到幻讀。
串行化(Serializable)
最高隔離級別,通過完全序列化事務來避免所有并發問題,這通常通過鎖定事務訪問的行來實現,性能開銷較大。
MVCC的具體實現
隱藏字段
InnoDB存儲引擎在每行數據的后面添加了三個隱藏字段:
1、DB_TRX_ID(6字節):記錄創建或最后一次更新該行的事務ID。
在InnoDB中,每個事務都有一個唯一的事務ID,叫做transaction
id(縮寫trx_id),它是在事務開始時候向InnoDB的事務系統申請的,并且按照申請順序嚴格遞增。在這里DB_TRX_ID就表示最近一次對該行數據作修改(insert或update)的事務ID。至于delete操作,InnoDB認為是一個update操作,不過會更新一個另外的刪除位,將行表示為deleted,并非真正刪除。
2、DB_ROLL_PTR(7字節):回滾指針,指向當前記錄行的undo log信息,用于回滾該行的舊版本
3、DB_ROW_ID(6字節):行標識,如果表沒有顯式的主鍵或唯一索引時使用。這個字段和MVCC關系不大,所以我們在這里不必關注。
這是隨著新行插入而單調遞增的行ID。理解:當表沒有主鍵或唯一非空索引時,InnoDB就會使用這個行ID自動產生聚簇索引。如果表有主鍵或唯一非空索引,聚簇索引就不會包含這個行ID了。
Read View(一致性視圖)
read view的真正作用是用來做可見性判斷的,里面保存了“對本事務不可見的其他活躍事務”。
按照可重復讀的定義,一個事務啟動的時候,能夠看到所有已經提交的事務結果。但是之后,這個事務執行期間,其他事務的更新對它不可見。因此,一個事務只需要在啟動的時候聲明說,“以我啟動的時刻為準,如果一個數據版本是在我啟動之前生成的,就認;如果是我啟動以后才生成的,我就不認,我必須要找到它的上一個版本”。當然,如果“上一個版本”也不可見,那就得繼續往前找。
Read View有4個重要的字段
1、m_ids :創建 Read View 時,當前數據庫中「活躍事務(啟動了但沒提交)」的事務 id 列表,注意是一個列表。
2、min_trx_id :創建 Read View 時,當前數據庫中「活躍事務」中事務 id 最小的事務,也就是 m_ids 的最小值。
3、max_trx_id :不是 m_ids 的最大值,而是創建 Read View 時當前數據庫中應該給下一個事務的 id 值,也就是全局事務中最大的事務 id 值 + 1;
4、creator_trx_id :指的是創建該 Read View 的事務的事務 id。
在可見性的實現上,InnoDB為每個事務構建了一個數組,用來保存這個事務啟動瞬間,當前正在”活躍“的所有事務ID。”活躍“指的是啟動了但還沒提交。
數組里面事務 ID 的最小值記為低水位,當前系統里面已經創建過的事務 ID 的最大值加 1 記為高水位。這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。這里需要注意:低水位到高水位之間的某些事務ID是沒在數組中的,沒在的原因是它們已經提交了,比如低水位為100,高水位為106,而數組中可能只有100、101、103、105這四個事務ID,104和102不在的原因是因為在當前事務啟動時,這兩個事務已經提交了。

3、如果落在黃色部分,那就包括兩種情況:
a. 如果DB_TRX_ID在數組中(也就說明這個事務在當前事務啟動時還活躍),那么表示這個這個版本是由還沒提交的事務生成的,不可見;(需要去undo log找可見版本)
b. 若 row trx_id 不在數組中,表示這個版本是已經提交了的事務生成的,可見。
讀提交和可重復讀的read view產生區別:
在innodb中的可重復讀級別, 只有事務在begin之后,執行第一條select(讀操作)時, 才會創建一個快照(read view),將當前系統中活躍的其他事務記錄起來;并且事務之后都是使用的這個快照,不會重新創建,直到事務結束。
在innodb中的讀提交級別, 事務在begin之后,執行每條select(讀操作)語句時,快照會被重置,即會重新創建一個快照(read view)。
undo log
undo log中存儲的是老版本數據,當一個事務需要讀取記錄行時,如果當前記錄行不可見,可以順著undo log鏈找到滿足其可見性條件的記錄行版本,這也是InnoDB利用”所有數據都有多個版本“這個特性,來實現可見性的核心。
下圖記錄了一行數據被多個事務連續更新后的狀態(圖中的row trx_id就是上面提到的DB_TRX_ID):
圖中虛線框內是同一行數據的四個版本,當前最新版本是 V4,k 的值是 22,它是被 事務ID 為 25 的事務更新的,因此它的 DB_TRX_ID 是 25。
在上圖中,三個虛線箭頭其實就代表了undo log;V1、V2、V3其實并不是物理上真實存在的,而是每次需要的時候根據當前版本和undo log計算出來的,比如,需要V2的時候,就是通過V4依次執行U3、U2計算出來。
比如,假如有一個事務的低水位是18,它要讀取上面圖中的數據,那么當它訪問時候,獲取了當前的DB_TRX_ID為25,假設這個25在數組中(說明這個25在事務啟動時依然活躍),那么因為25高于低水位,所以對于當前事務來說不可見,于是這個事務就會從V4通過U3計算得出V3,V3的DB_TRX_ID=17小于18,所以這個數據是可見的,所以對于當前事務來講,這個事務的值通過undo log就可以構造出來,為11。
大多數對數據的變更操作包含insert/update/delete,在InnoDB里,undo log分為如下兩類:
insert undo log:事務insert新記錄時產生的undo log,只在事務回滾時需要,并且在事務提交后就可以立即丟棄
update undo log:事務對記錄進行delete和update操作時產生的undo log,不僅在事務回滾時需要,快照讀也需要,只有當數據庫所使用的快照不涉及該日志記錄,對應的回滾日志才會被purge線程刪除。
Purge線程: 為了實現InnoDB的MVCC機制,更新或者刪除操作都只是設置一下舊記錄的deleted_bit,并不真正將舊記錄刪除。
為了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit為true的記錄。purge線程自己也維護了一個read
view,如果某個記錄的deleted_bit為true,并且DB_TRX_ID相對于purge線程的read
view可見,那么這條記錄一定是可以被安全清除的。
在MySQL InnoDB引擎中,各隔離級別是如何實現的呢?
讀未提交(Read Uncommitted)
InnoDB實際上并不直接支持此隔離級別,因為這會引發臟讀、不可重復讀和幻讀等問題。如果要實現的話,數據庫可允許事務讀取其他事務尚未提交的數據,不做任何額外的并發控制即可。
讀已提交(Read Committed)
InnoDB通過多版本并發控制(MVCC)機制實現。在讀已提交的隔離級別下,每個事務在每次讀取數據時都會生成一個自己的讀視圖(Read View)。這個視圖是由事務開始時正在提交的事務所影響的數據項的快照構成的。
具體實現上,MySQL會在每行數據后添加3個隱藏的列來實現MVCC,這3個列分別是:
1、DB_TRX_ID:記錄創建或最后一次更新該行的事務ID。
2、DB_ROLL_PTR:指向回滾段的指針,用于回滾該行的舊版本。
3、DB_ROW_ID:行標識,如果表沒有顯式的主鍵或唯一索引時使用。這個字段和MVCC關系不大,所以我們在這里不必關注。
當事務需要讀取數據時,它會讀取DB_TRX_ID不為當前事務ID的行,即已提交事務的數據。
可重復讀(Repeatable Read)
這是InnoDB默認的隔離級別,InnoDB也是通過MVCC機制來實現可重復讀隔離級別的。可重復讀隔離級別是啟動事務時生成一個 Read View,然后整個事務期間都在用這個 Read View。
MVCC機制為每個事務分配一個唯一的事務ID,并記錄每行數據的創建版本號和刪除版本號,確保在同一個事務內多次讀取同一數據時結果一致,解決了不可重復讀的問題。MVCC通過數據行的隱藏列(例如事務ID、回滾指針等)以及undo日志來管理多個事務對同一數據的并發訪問,確保事務看到的數據在事務期間保持一致,即便其他事務已經修改或刪除了這些數據。
MVCC通過維護數據的多個版本來實現事務的隔離性,而無需依賴傳統的鎖機制(雖然InnoDB也使用鎖,但主要是為了解決寫沖突)。每個事務看到的數據是由該事務的開始時間點決定的,這保證了在可重復讀級別下,即使其他事務提交了新的數據,當前事務仍然能夠看到它開始時的數據狀態,避免了臟讀、不可重復讀的問題,但幻讀仍可能在某些場景下發生,除非使用了Next-Key Locks或者將隔離級別調整為串行化。
選擇該隔離級別是因為主從同步如果先后讀取不一致,可能會出現主從同步問題。
串行化(Serializable)
雖然InnoDB支持串行化隔離級別,但實際應用中較少使用,因為它通過完全鎖定讀取的行來防止并發修改,這會嚴重影響系統的并發性能。在串行化級別下,InnoDB會對涉及的行加鎖,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖沖突的時候,后訪問的事務必須等前一個事務執行完成,阻止其他事務并發修改,以此實現最高的隔離性。