導讀:
來自網易研究院的MySQL內核技術研究人何登成,把MySQL數據庫InnoDB存儲引擎的多版本控制(簡稱:MVCC)實現原理,做了深入的研究與詳細的文字圖表分析,方便大家理解InnoDB存儲引擎實現的多版本控制技術(簡稱:MVCC)。
假設對于多版本控制(MVCC)的基礎知識,有所了解。MySQL數據庫InnoDB存儲引擎為了實現多版本的一致性讀,采用的是基于回滾段的協議。
MySQL數據庫InnoDB存儲引擎表數據的組織方式為主鍵聚簇索引。由于采用索引組織表結構,記錄的ROWID是可變的(索引頁的時候,Structure Modification Operation,SMO),因此二級索引中采用的是(索引鍵值, 主鍵鍵值)的組合來唯一確定一條記錄。
無論是聚簇索引,還是二級索引,其每條記錄都包含了一個DELETED BIT位,用于標識該記錄是否是刪除記錄。除此之外,聚簇索引記錄還有兩個系統列:DATA_TRX_ID,DATA_ROLL_PTR。DATA _TRX_ID表示產生當前記錄項的事務ID;DATA _ROLL_PTR指向當前記錄項的undo信息。
聚簇索引行結構(與多版本一致讀有關的部分,DELETED BIT省略):
二級索引行結構:
從聚簇索引行結構,與二級索引行結構可以看出,聚簇索引中包含版本信息(事務號+回滾指針),二級索引不包含版本信息,二級索引項的可見性如何判斷?下面將會給出。
InnoDB存儲引擎默認的隔離級別為Repeatable Read (RR),可重復讀。InnoDB存儲引擎在開始一個RR讀之前,會創建一個Read View。Read View用于判斷一條記錄的可見性。Read View定義在read0read.h文件中,其中最主要的與可見性相關的屬性如下:
?1234567101112131415161718192021 |
|
簡單來說,Read View記錄讀開始時,所有的活動事務,這些事務所做的修改對于Read View是不可見的。除此之外,所有其他的小于創建Read View的事務號的所有記錄均可見??梢姲▋蓪雍x:
123456710111213141516 |
- Insert
|
-read隔離級別
repeatable read(RR)
代碼調用流程:
?1 | ha_innobase::update_row -> row_update_for_mysql -> row_upd_step -> row_upd -> row_upd_clust_step -> row_upd_clust_rec_by_insert -> btr_cur_del_mark_set_clust_rec -> row_ins_index_entry |
簡單來說,就是將cluster index的舊記錄標記位刪除;插入一條新紀錄。該語句執行完之后,數據結構如下:
老版本仍舊存儲在聚簇索引之中,其DATA_TRX_ID被設置為1811,Deleted bit設置為1,undo中記錄了前鏡像的事務id = 1809。新版本DATA_TRX_ID也為1811。通過此圖,還可以發現,雖然新老版本是一條記錄,但是在聚簇索引中是通過兩條記錄來標識的。同時, 由于更新了主鍵,二級索引也需要做相應的更新(二級索引中包含主鍵項)。
更新comment字段,代碼調用流程與上面有部分不同,可以自行跟蹤,此處省略。更新操作執行完之后,索引結構變更如下:
從上圖可見,更新二級索引的鍵值時,聚簇索引本身并不會產生新的記錄項,而是將舊版本信息記錄在undo之中。與此同時,二級索引將會產生 新的索引項,其PK值保持不變,指向聚簇索引的同一條記錄。細心的讀者可能會發現,二級索引頁面中有一個MAX_TRX_ID,此值記錄的是更新二級索引 頁面的最大事務ID。通過MAX_TRX_ID的過濾,INNODB能夠實現大部分的輔助索引覆蓋性掃描(僅僅掃描輔助索引,不需要回聚簇索引)。具體過 濾方法,將在后面的內容中給出。
最后一個測試用例,是更新comment項為同樣的值。在我的測試中,更新之后的索引結構如下:
聚簇索引仍舊會更新,但是二級索引保持不變。
select * from test where id = 1;
針對測試1,如果up_limit_id,low_limit_id都無法判斷可見性,那么遍歷read_view中的trx_ids,依次對比事務id,如果在DATA_TRX_ID在trx_ids數組中,則不可見(更新未提交)。
select * from test where id = 9;
針對測試2,如果1811不可見,無結果返回。
select * from test where id > 0;
總結:
select comment from test where comment > ‘ ‘;
針對測試2,二級索 引,如果當前頁面不能滿足MAX_TRX_ID < read_view.up_limit_id,說明當前頁面無法進行索引覆蓋性掃描,此時需要針對每一項,到聚簇索引中判斷可見性。回到測試2,二級索引 中有兩項pk = 9 (一項deleted bit = 1,另一個為0),對應的聚簇索引中只有一項pk= 9。如何保證通過二級索引過來的同一記錄的多個版本,在聚簇索引中最多只能被返回一次?如果當前事務id 1811可見。二級索引pk = 9的記錄(兩項),通過聚簇索引的undo,都定位到了同一記錄項。此時,InnoDB通過以下的一個表達式,來保證來自二級索引,指向同一聚簇索引記錄 的多個版本項,有且最多僅有一個版本將會返回數據:
?1234567 | if (clust_rec
rec,dict_table_is_comp(sec_index->table))) && !row_sel_sec_rec_is_for_clust_rec(rec, sec_index, clust_rec, clust_index)) |
滿足if判斷的所有聚簇索引記錄,都直接丟棄,以上判斷的邏輯如下:
為什么滿足if判斷,就可以直接丟棄數據?用白話來說,就是我們通過二級索引記錄,定位聚簇索引記錄,定位之后,還需要再次檢查聚簇索引記錄是否仍舊是我在二級索引中看到的記錄。如果不是,則直接丟棄;如果是,則返回。
根據此條件,結合查詢與測試2中的索引結構。可見版本為事務1811.二級索引中的兩項pk = 9都能通過聚簇索引回滾到1811版本。但是,二級索引記錄(ccc,9)與聚簇索引回滾后的版本(aaa,9)不一致,直接丟棄。只有二級索引記錄 (aaa,9)保持一致,直接返回。
總結:
Purge功能:
InnoDB由于要支持多版本協議,因此無論是更新,刪除,都只是設置記錄上的deleted bit標記位,而不是真正的刪除記錄。后續這些記錄的真正刪除,是通過Purge后臺進程實現的。Purge進程定期掃描InnoDB的undo,按照先 讀老undo,再讀新undo的順序,讀取每條undo record。對于每一條undo record,判斷其對應的記錄是否可以被purge(purge進程有自己的read view,等同于進程開始時最老的活動事務之前的view,保證purge的數據,一定是不可見數據,對任何人來說),如果可以purge,則構造完整記 錄(row_purge_parse_undo_rec)。然后按照先purge二級索引,最后purge聚簇索引的順序,purge一個操作生成的舊版本完整記錄。
一個完整的purge函數調用流程如下:
?123 | row_purge_step->row_purge->trx_purge_fetch_next_rec->row_purge_parse_undo_rec ->row_purge_del_mark->row_purge_remove_sec_if_poss ->row_purge_remove_clust_if_poss |
總結:
文章具體來源不詳,如有知情者,請在評論中回復。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com