天天看點

mysql二級索引沒有mvcc_MySQL InnoDB MVCC深度分析

關于MySQL的InnoDB的MVCC原理,很多朋友都能說個大概:

每行記錄都含有兩個隐藏列,分别是記錄的建立時間與删除時間

每次開啟事務都會産生一個全局自增ID

在RR隔離級别下

INSERT ->  記錄的建立時間 = 目前事務ID,删除時間 = NULL

DELETE -> 記錄的建立時間不動,删除時間 = 目前事務ID

UPDATE -> 将記錄複制一次

老記錄的建立時間不動,删除時間 = 目前事務ID

新記錄的建立時間 = 目前事務ID,删除時間 = NULL

SELECT -> 傳回的記錄需要滿足兩個條件:

建立時間 <= 目前事務ID (記錄是在目前事務之前或者由目前事務建立的)

删除時間 == NULL || 删除時間 > 目前事務ID (記錄是在目前事務之後被删除的)

但實際上,這個描述是很不嚴格的,問題有以下幾點:

1. 每條記錄含有的隐藏列不是兩個而是三個

它們分别是:

DB_TRX_ID, 6byte, 建立這條記錄/最後一次更新這條記錄的事務ID

DB_ROLL_PTR, 7byte,復原指針,指向這條記錄的上一個版本(存儲于rollback segment裡)

DB_ROW_ID, 6byte,隐含的自增ID,如果資料表沒有主鍵,InnoDB會自動以DB_ROW_ID産生一個聚簇索引

2. 記錄的曆史版本是放在專門的rollback segment裡(undo log)

UPDATE非主鍵語句的效果是

老記錄被複制到rollback segment中形成undo log,DB_TRX_ID和DB_ROLL_PTR不動

新記錄的DB_TRX_ID = 目前事務ID,DB_ROLL_PTR指向老記錄形成的undo log

這樣就能通過DB_ROLL_PTR找到這條記錄的曆史版本。如果對同一行記錄執行連續的update操作,新記錄與undo log會組成一個連結清單,周遊這個連結清單可以看到這條記錄的變遷)

3. MySQL的一緻性讀,是通過一個叫做read view的結構來實作的

read_view中維護了系統中活躍事務集合的快照,這些活躍事務ID的最小值為up_limit_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_tup_limit_id; // The read should see all trx ids which are strictly smaller (

SELECT操作傳回結果的可見性是由以下規則決定的:

DB_TRX_ID < up_limit_id  -> 此記錄的最後一次修改在read_view建立之前,可見

DB_TRX_ID > low_limit_id   -> 此記錄的最後一次修改在read_view建立之後,不可見  ->  需要用DB_ROLL_PTR查找undo log(此記錄的上一次修改),然後根據undo log的DB_TRX_ID再計算一次可見性

up_limit_id <= DB_TRX_ID <= low_limit_id -> 需要進一步檢查read_view中是否含有DB_TRX_ID

DB_TRX_ID ∉ read_view  -> 此記錄的最後一次修改在read_view建立之前,可見

DB_TRX_ID ∈ read_view -> 此記錄的最後一次修改在read_view建立時尚未儲存,不可見  ->  需要用DB_ROLL_PTR查找undo log(此記錄的上一次修改),然後根據undo log的DB_TRX_ID再從頭計算一次可見性

經過上述規則的決議,我們得到了這條記錄相對read_view來說,可見的結果。

此時,如果這條記錄的delete_flag為true,說明這條記錄已被删除,不傳回。

如果delete_flag為false,說明此記錄可以安全傳回給用戶端

4. 用MVCC這一種手段可以同時實作RR與RC隔離級别

它們的不同之處在于:

RR:read view是在first touch read時建立的,也就是執行事務中的第一條SELECT語句的瞬間,後續所有的SELECT都是複用這個read view,是以能保證每次讀取的一緻性(可重複讀的語義)

RC:每次讀取,都會建立一個新的read view。這樣就能讀取到其他事務已經COMMIT的内容。

是以對于InnoDB來說,RR雖然比RC隔離級别高,但是開銷反而相對少。

補充:RU的實作就簡單多了,不使用read view,也不需要管什麼DB_TRX_ID和DB_ROLL_PTR,直接讀取最新的record即可。

5. 二級索引與MVCC

MySQL的索引分為聚簇索引(clustered index)與二級索引(secondary index)兩種。

剛才講的内容是基于聚簇索引的,隻有聚簇索引中含有DB_TRX_ID與DB_ROLL_PTR隐藏列,可以比較容易的實作MVCC

但是二級索引中并不含有這幾個隐藏列,隻含有1個bit的deleted flag,咋辦?

好辦,如果UPDATE語句涉及到二級索引的鍵值,将老的二級索引的deleted flag标記為true,然後建立一條新的二級索引記錄即可。

但是如果想根據二級索引來做查詢,這可就麻煩了。因為二級索引不維護版本資訊,無法判斷二級索引中記錄的可見性。

是以還是需要回到聚簇索引中來:

根據二級索引維護的主鍵值去聚簇索引中查找記錄(使用MVCC規則)

如果查出來的結果跟二級索引裡維護的結果相同 -> 傳回,如果不同 -> 丢棄

如果對于一條查詢語句,二級索引中有很多條滿足條件的結果(連續多次更新,導緻二級索引中有很多條記錄),那上面這個流程就比較低效了。是以InnoDB的作者搞了個機智的小優化:

在二級索引中,用一個額外的名為MAX_TRX_ID的變量來記錄最後一次更新二級索引的事務的ID

那麼,如果目前語句關聯的read_view的 up_limit_id > MAX_TRX_ID,說明在建立read_view時最後一次更新二級索引的事務已經結束,也就是說二級索引裡的所有記錄對于目前查詢都是可見的,此時可以直接根據二級索引的deleted flag來确定記錄是否應該被傳回。

小結一下:二級索引的MVCC可見性判斷在MAX_TRX_ID失效的情況下需要依賴聚簇索引才能完成。

6. purge

從前面的分析可以看出,為了實作InnoDB的MVCC機制,更新或者删除操作都隻是設定一下老記錄的deleted_bit,并不真正将過時的記錄删除。

為了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit為true的記錄。

為了不影響MVCC的正常工作,purge線程自己也維護了一個read view(這個read view相當于系統中最老活躍事務的read view)

如果某個記錄的deleted_bit為true,并且DB_TRX_ID相對于purge線程的read view可見,那麼這條記錄一定是可以被安全清除的。

參考文獻

InnoDB多版本(MVCC)實作簡要分析(水準很高,分析深入,必須要看,但可能不太好了解)