MySQL · 引擎特性 · InnoDB undo log 漫遊
本文是對整個Undo生命週期過程的闡述,程式碼分析基於當前最新的MySQL5.7版本。本文也可以作為瞭解整個Undo模組的代碼導讀。由於涉及到的模組眾多,因此部分細節並未深入。
前言
Undo log是InnoDB MVCC事務特性的重要組成部分。當我們對記錄做了變更操作時就會產生undo記錄,Undo記錄預設被記錄到系統資料表空間(ibdata)中,但從5.6開始,也可以使用獨立的Undo 資料表空間。
Undo記錄中儲存的是老版本資料,當一箇舊的事務需要讀取資料時,為了能讀取到老版本的資料,需要順著undo鏈找到滿足其可見度的記錄。當版本鏈很長時,通常可以認為這是個比較耗時的操作(例如bug#69812)。
大多數對資料的變更操作包括INSERT/DELETE/UPDATE,其中INSERT操作在事務提交前只對當前事務可見,因此產生的Undo日誌可以在事務提交後直接刪除(誰會對剛插入的資料有可見度需求呢!!),而對於UPDATE/DELETE則需要維護多版本資訊,在InnoDB裡,UPDATE和DELETE操作產生的Undo日誌被歸成一類,即update_undo。
基本檔案結構
為了保證事務並行作業時,在寫各自的undo log時不產生衝突,InnoDB採用復原段的方式來維護undo log的並發寫入和持久化。復原段實際上是一種 Undo 檔案組織方式,每個復原段又有多個undo log slot。具體的檔案組織方式如所示:
展示了基本的Undo復原段布局結構,其中:
- rseg0預留在系統資料表空間ibdata中;
- rseg 1~rseg 32這32個復原段存放於暫存資料表的系統資料表空間中;
- rseg33~ 則根據配置存放到獨立undo資料表空間中(如果沒有開啟獨立Undo資料表空間,則存放於ibdata中)
如果我們使用獨立Undo tablespace,則總是從第一個Undo space開始輪詢分配undo 復原段。大多數情況下這是OK的,但假設我們將復原段的個數從33開始依次遞增配置到128,就可能導致所有的復原段都存放在同一個undo space中。(參考函數trx_sys_create_rsegs 以及 bug#74471)
每個復原段維護了一個段頭頁,在該page中又劃分了1024個slot(TRX_RSEG_N_SLOTS),每個slot又對應到一個undo log對象,因此理論上InnoDB最多支援 96 * 1024個普通事務。
MySQL InnoDB表--BTree基本資料結構
在MySQL的InnoDB儲存引擎中count(*)函數的最佳化
MySQL InnoDB儲存引擎鎖機制實驗
InnoDB儲存引擎的啟動、關閉與恢複
MySQL InnoDB獨立資料表空間的配置
MySQL Server 層和 InnoDB 引擎層 體繫結構圖
InnoDB 死結案例解析
MySQL Innodb獨立資料表空間的配置
關鍵結構體
為了便於管理和使用undo記錄,在記憶體中維持了如下關鍵結構體對象:
- 所有復原段都記錄在
trx_sys->rseg_array
,數組大小為128,分別對應不同的復原段;
- rseg_array數群組類型為trx_rseg_t,用於維護復原段相關資訊;
- 每個復原段對象trx_rseg_t還要管理undo log資訊,對應結構體為trx_undo_t,使用多個鏈表來維護trx_undo_t資訊;
- 事務開啟時,會專門給他指定一個復原段,以後該事務用到的undo log頁,就從該復原段上分配;
- 事務提交後,需要purge的復原段會被放到purge隊列上(
purge_sys->purge_queue
)。
各個結構體之間的聯絡如下:
分配復原段
當開啟一個讀寫事務時(或者從唯讀事務轉換為讀寫事務),我們需要預先為事務分配一個復原段:
對於唯讀事務,如果產生對暫存資料表的寫入,則需要為其分配復原段,使用暫存資料表復原段(第1~32號復原段),函數入口:trx_assign_rseg -->trx_assign_rseg_low-->get_next_noredo_rseg
。
在MySQL5.7中事務預設以唯讀事務開啟,當隨後判定為讀寫事務時,則轉換成讀寫入模式,並為其分配事務ID和復原段,調用函數:trx_set_rw_mode -->trx_assign_rseg_low --> get_next_redo_rseg
。
普通復原段的分配方式如下:
- 採用round-robin的輪詢方式來賦予復原段給事務,如果復原段被標記為skip_allocation(這個undo tablespace太大了,purge線程需要對其進行truncate操作),則跳到下一個;
- 選擇一個復原段給事務後,會將該復原段的
rseg->trx_ref_count
遞增,這樣該復原段所在的undo tablespace檔案就不可以被truncate掉;
- 暫存資料表復原段被賦予
trx->rsegs->m_noredo
,普通讀寫操作的復原段被賦予trx->rsegs->m_redo
;如果事務在唯讀階段使用到暫存資料表,隨後轉換成讀寫事務,那麼會為該事務分配兩個復原段。
使用復原段
當產生資料變更時,我們需要使用Undo log記錄下變更前的資料以維護多版本資訊。insert 和 delete/update 分開記錄undo,因此需要從復原段單獨分配Undo slot。
入口函數:trx_undo_report_row_operation
流程如下:
- 判斷當前變更的是否是暫存資料表,如果是暫存資料表,則採用暫存資料表復原段來分配,否則採用普通的復原段;
- 暫存資料表操作記錄undo時不寫redo log;
- 操作類型為TRX_UNDO_INSERT_OP,且未分配insert undo slot時,調用函數
trx_undo_assign_undo
進行分配;
- 操作類型為TRX_UNDO_MODIFY_OP,且未分配Update undo slot時,調用函數
trx_undo_assign_undo
進行分配。
我們來看看函數trx_undo_assign_undo的流程:
- 首先總是從cahced list上分配trx_undo_t (函數
trx_undo_reuse_cached
,當滿足某些條件時,事務提交時會將其擁有的trx_undo_t放到cached list上,這樣新的事務可以重用這些undo 對象,而無需去掃描復原段,尋找可用的slot,在後面的事務提交一節會介紹到);
- 對於INSERT,從
trx_rseg_t::insert_undo_cached
上擷取,並修改頭部重用資訊(trx_undo_insert_header_reuse)及預留XID空間(trx_undo_header_add_space_for_xid)
- 對於DELETE/UPDATE,從
trx_rseg_t::update_undo_cached
上擷取, 並在undo log hdr page上建立新的Undo log header(trx_undo_header_create),及預留XID儲存空間(trx_undo_header_add_space_for_xid)
- 擷取到trx_undo_t對象後,會從cached list上移除掉。並初始化trx_undo_t相關資訊(trx_undo_mem_init_for_reuse),將
trx_undo_t::state
設定為TRX_UNDO_ACTIVE
如果沒有cache的trx_undo_t,則需要從復原段上分配一個閒置undo slot(trx_undo_create),並建立對應的undo頁,進行初始化;
一個復原段可以支援1024個事務並發,如果不幸復原段都用完了(通常這幾乎不會發生),會返回錯誤DB_TOO_MANY_CONCURRENT_TRXS
每一個Undo log segment實際上對應一個獨立的段,段頭的起始位置在UNDO 頭page的TRX_UNDO_SEG_HDR+TRX_UNDO_FSEG_HEADER位移位置(見)
已指派給事務的trx_undo_t會加入到鏈表trx_rseg_t::insert_undo_list
或者trx_rseg_t::update_undo_list上
;
- 如果是資料詞典操作(DDL)產生的undo,主要是表層級操作,例如建立或刪除表,還需要記錄操作的table id到undo log header中(TRX_UNDO_TABLE_ID),同時將TRX_UNDO_DICT_TRANS設定為TRUE。(trx_undo_mark_as_dict_operation)。
總的來說,undo header page主要包括如下資訊:
如何寫入undo日誌
入口函數:trx_undo_report_row_operation
當分配了一個undo slot,同時初始化完可用的空閑地區後,就可以向其中寫入undo記錄了。寫入的page no取自undo->last_page_no
,初始情況下和hdr_page_no相同。
對於INSERT_UNDO,調用函數trx_undo_page_report_insert進行插入,記錄格式大致如所示:
對於UPDATE_UNDO,調用函數trx_undo_page_report_modify
進行插入,UPDATE UNDO的記錄格式大概如所示:
在寫入的過程中,可能出現單頁面空間不足的情況,導致寫入失敗,我們需要將剛剛寫入的地區清空重設(trx_undo_erase_page_end),同時申請一個新的page(trx_undo_add_page) 加入到undo log段上,同時將undo->last_page_no
指向新分配的page,然後重試。
完成Undo log寫入後,構建新的復原段指標並返回(trx_undo_build_roll_ptr),復原段指標包括undo log所在的復原段id、日誌所在的page no、以及page內的位移量,需要記錄到叢集索引記錄中。
更多詳情見請繼續閱讀下一頁的精彩內容: