關於MySQL的InnoDB的MVCC原理,不少朋友都能說個大概:html
每行記錄都含有兩個隱藏列,分別是記錄的建立時間與刪除時間mysql
每次開啓事務都會產生一個全局自增IDgit
在RR隔離級別下github
INSERT -> 記錄的建立時間 = 當前事務ID,刪除時間 = NULLsql
DELETE -> 記錄的建立時間不動,刪除時間 = 當前事務ID安全
UPDATE -> 將記錄複製一次mvc
老記錄的建立時間不動,刪除時間 = 當前事務ID優化
新記錄的建立時間 = 當前事務ID,刪除時間 = NULLthis
SELECT -> 返回的記錄須要知足兩個條件:.net
建立時間 <= 當前事務ID (記錄是在當前事務以前或者由當前事務建立的)
刪除時間 == NULL || 刪除時間 > 當前事務ID (記錄是在當前事務以後被刪除的)
但實際上,這個描述是很不嚴格的,問題有如下幾點:
它們分別是:
DB_TRX_ID, 6byte, 建立這條記錄/最後一次更新這條記錄的事務ID
DB_ROLL_PTR, 7byte,回滾指針,指向這條記錄的上一個版本(存儲於rollback segment裏)
DB_ROW_ID, 6byte,隱含的自增ID,若是數據表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引
另外,每條記錄的頭信息(record header)裏都有一個專門的bit(deleted_flag)來表示當前記錄是否已經被刪除
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會組成一個鏈表,遍歷這個鏈表能夠看到這條記錄的變遷)
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_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".
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,說明此記錄能夠安全返回給客戶端
它們的不一樣之處在於:
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便可。
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失效的狀況下須要依賴聚簇索引才能完成。
從前面的分析能夠看出,爲了實現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)實現簡要分析(水平很高,分析深刻,必需要看,但可能不太好理解)