MySQL·引擎特性·InnoDBundolog漫遊
本文是對整個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個普通事務。
關鍵結構體
為了便於管理和使用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
- 對於INSERT,從
- 如果沒有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內的偏移量,需要記錄到聚集索引記錄中。
事務Prepare階段
入口函式:trx_prepare_low
當事務完成需要提交時,為了和BINLOG做XA,InnoDB的commit被劃分成了兩個階段:prepare階段和commit階段,本小節主要討論下prepare階段undo相關的邏輯。
為了在崩潰重啟時知道事務狀態,需要將事務設定為Prepare,MySQL 5.7對臨時表undo和普通表undo分別做了處理,前者在寫undo日誌時總是不需要記錄redo,後者則需要記錄。
分別設定insert undo 和 update undo的狀態為prepare,呼叫函式trx_undo_set_state_at_prepare,過程也比較簡單,找到undo log slot對應的頭頁面(trx_undo_t::hdr_page_no),將頁面段頭的TRX_UNDO_STATE設定為TRX_UNDO_PREPARED,同時修改其他對應欄位,如下圖所示(對於外部顯式XA所產生的XID,這裡不做討論):
Tips:InnoDB層的XID是如何獲取的呢? 當Innodb的引數innodb_support_xa開啟時,在執行事務的第一條SQL時,就會去註冊XA,根據第一條SQL的query id拼湊XID資料,然後儲存在事務物件中。參考函式trans_register_ha
。
事務Commit
當事務commit時,需要將事務狀態設定為COMMIT狀態,這裡同樣通過Undo來實現的。
入口函式:trx_commit_low-->trx_write_serialisation_history
在該函式中,需要將該事務包含的Undo都設定為完成狀態,先設定insert undo,再設定update undo(trx_undo_set_state_at_finish),完成狀態包含三種:
- 如果當前的undo log只佔一個page,且佔用的header page大小使用不足其3/4時(TRX_UNDO_PAGE_REUSE_LIMIT),則狀態設定為TRX_UNDO_CACHED,該undo物件會隨後加入到undo cache list上;
- 如果是Insert_undo(undo型別為TRX_UNDO_INSERT),則狀態設定為TRX_UNDO_TO_FREE;
- 如果不滿足a和b,則表明該undo可能需要Purge執行緒去執行清理操作,狀態設定為TRX_UNDO_TO_PURGE。
在確認狀態資訊後,寫入undo header page的TRX_UNDO_STATE中。
如果當前事務包含update undo,並且undo所在回滾段不在purge佇列時,還需要將當前undo所在的回滾段(及當前最大的事務號)加入Purge執行緒的Purge佇列(purge_sys->purge_queue)中(參考函式trx_serialisation_number_get
)。
對於undate undo需要呼叫trx_undo_update_cleanup
進行清理操作,清理的過程包括:
-
將undo log加入到history list上,呼叫
trx_purge_add_update_undo_to_history
:- 如果該undo log不滿足cache的條件(狀態為TRX_UNDO_CACHED,如上述),則將其佔用的slot設定為FIL_NULL,意為slot空閒,同時更新回滾段頭的TRX_RSEG_HISTORY_SIZE值,將當前undo佔用的page數累加上去;
- 將當前undo加入到回滾段的TRX_RSEG_HISTORY連結串列上,作為連結串列頭節點,節點指標為UNDO頭的TRX_UNDO_HISTORY_NODE;
- 更新
trx_sys->rseg_history_len
(也就是show engine innodb status看到的history list),如果只有普通的update_undo,則加1,如果還有臨時表的update_undo,則加2,然後喚醒purge執行緒; - 將當前事務的
trx_t::no
寫入undo頭的TRX_UNDO_TRX_NO段; - 如果不是delete-mark操作,將undo頭的TRX_UNDO_DEL_MARKS更新為false;
-
如果undo所在回滾段的
rseg->last_page_no
為FIL_NULL,表示該回滾段的舊的清理已經完成,進行如下賦值,記錄這個回滾段上第一個需要purge的undo記錄資訊:rseg->last_page_no = undo->hdr_page_no; rseg->last_offset = undo->hdr_offset; rseg->last_trx_no = trx->no; rseg->last_del_marks = undo->del_marks;
- 如果undo需要cache,將undo物件放到回滾段的update_undo_cached連結串列上;否則釋放undo物件(trx_undo_mem_free)。
注意上面只清理了update_undo,insert_undo直到事務釋放記錄鎖、從讀寫事務連結串列清除、以及關閉read view後才進行,呼叫函式trx_undo_insert_cleanup:
- 如果Undo狀態為TRX_UNDO_CACHED,則加入到回滾段的insert_undo_cached連結串列上;
- 否則,將該undo所佔的segment及其所佔用的回滾段的slot全部釋放掉(trx_undo_seg_free),修改當前回滾段的大小(rseg->curr_size),並釋放undo物件所佔的記憶體(trx_undo_mem_free),和Update_undo不同,insert_undo並未放到History list上。
事務完成提交後,需要將其使用的回滾段引用計數rseg->trx_ref_count減1;
事務回滾
如果事務因為異常或者被顯式的回滾了,那麼所有資料變更都要改回去。這裡就要藉助回滾日誌中的資料來進行恢復了。
入口函式為:row_undo_step --> row_undo
操作也比較簡單,析取老版本記錄,做逆向操作即可:對於標記刪除的記錄清理標記刪除標記;對於in-place更新,將資料回滾到最老版本;對於插入操作,直接刪除聚集索引和二級索引記錄(row_undo_ins)。
具體的操作中,先回滾二級索引記錄(row_undo_mod_del_mark_sec、row_undo_mod_upd_exist_sec、row_undo_mod_upd_del_sec),再回滾聚集索引記錄(row_undo_mod_clust)。這裡不展開描述,可以參閱對應的函式。
多版本控制
InnoDB的多版本使用undo來構建,這很好理解,undo記錄中包含了記錄更改前的映象,如果更改資料的事務未提交,對於隔離級別大於等於read commit的事務而言,它不應該看到已修改的資料,而是應該給它返回老版本的資料。
入口函式: row_vers_build_for_consistent_read
由於在修改聚集索引記錄時,總是儲存了回滾段指標和事務id,可以通過該指標找到對應的undo 記錄,通過事務Id來判斷記錄的可見性。當舊版本記錄中的事務id對當前事務而言是不可見時,則繼續向前構建,直到找到一個可見的記錄或者到達版本鏈尾部。(關於事務可見性及read view,可以參閱我們之前的月報)
Tips 1:構建老版本記錄(trx_undo_prev_version_build
)需要持有page latch,因此如果Undo鏈太長的話,其他請求該page的執行緒可能等待時間過長導致crash,最典型的就是備庫備份場景:
當備庫使用innodb表儲存複製位點資訊時(relay_log_info_repository=TABLE),邏輯備份顯式開啟一個read view並且執行了長時間的備份時,這中間都無法對slave_relay_log_info表做purge操作,導致版本鏈極其長;當開始備份slave_relay_log_info表時,就需要去花很長的時間構建老版本;複製執行緒由於需要更新slave_relay_log_info表,因此會陷入等待Page latch的場景,最終有可能導致訊號量等待超時,例項自殺。 (bug#74003)
Tips 2:在構建老版本的過程中,總是需要建立heap來儲存舊版本記錄,實際上這個heap是可以重用的,無需總是重複構建(bug#69812)
Tips 3:如果回滾段型別是INSERT,就完全沒有必要去看Undo日誌了,因為一個未提交事務的新插入記錄,對其他事務而言總是不可見的。
Tips 4: 對於聚集索引我們知道其記錄中存有修改該記錄的事務id,我們可以直接判斷是否需要構建老版本(lock_clust_rec_cons_read_sees
),但對於二級索引記錄,並未儲存事務id,而是每次更新記錄時,同時更新記錄所在的page上的事務id(PAGE_MAX_TRX_ID),如果該事務id對當前事務是可見的,那麼就無需去構建老版本了,否則就需要去回表查詢對應的聚集索引記錄,然後判斷可見性(lock_sec_rec_cons_read_sees
)。
Purge清理操作
從上面的分析我們可以知道:update_undo產生的日誌會放到history list中,當這些舊版本無人訪問時,需要進行清理操作;另外頁內標記刪除的操作也需要從物理上清理掉。後臺Purge執行緒負責這些工作。
入口函式:srv_do_purge --> trx_purge
- 確認可見性
在開始嘗試purge前,purge執行緒會先克隆一個最老的活躍檢視(trx_sys->mvcc->clone_oldest_view
),所有在readview開啟之前提交的事務所做的事務變更都是可以清理的。
- 獲取需要purge的undo記錄(
trx_purge_attach_undo_recs
)
從history list上讀取多個Undo記錄,並分配到多個purge執行緒的工作佇列上((purge_node_t*) thr->child->undo_recs
),預設一次最多取300個undo記錄,可通過引數innodb_purge_batch_size引數調整。
- Purge工作執行緒
當完成任務的分發後,各個工作執行緒(包括協調執行緒)開始進行purge操作
入口函式: row_purge_step -> row_purge -> row_purge_record_func
主要包括兩種:一種是記錄直接被標記刪除了,這時候需要物理清理所有的聚集索引和二級索引記錄(row_purge_record_func
);另一種是聚集索引in-place更新了,但二級索引上的記錄順序可能發生變化,而二級索引的更新總是標記刪除 + 插入,因此需要根據回滾段記錄去檢查二級索引記錄序是否發生變化,並執行清理操作(row_purge_upd_exist_or_extern
)。
- 清理history list
從前面的分析我們知道,insert undo在事務提交後,Undo segment 就釋放了。而update undo則加入了history list,為了將這些檔案空間回收重用,需要對其進行truncate操作;預設每處理128輪Purge迴圈後,Purge協調執行緒需要執行一次purge history List操作。
入口函式:trx_purge_truncate --> trx_purge_truncate_history
從回滾段的HISTORY 檔案連結串列上開始遍歷釋放Undo log segment,由於history 連結串列是按照trx no有序的,因此遍歷truncate直到完全清除,或者遇到一個還未purge的undo log(trx no比當前purge到的位置更大)時才停止。
關於Purge操作的邏輯實際上還算是比較複雜的程式碼模組,這裡只是簡單的介紹了下,以後有時間再展開描述。
崩潰恢復
當例項從崩潰中恢復時,需要將活躍的事務從undo中提取出來,對於ACTIVE狀態的事務直接回滾,對於Prepare狀態的事務,如果該事務對應的binlog已經記錄,則提交,否則回滾事務。
實現的流程也比較簡單,首先先做redo (recv_recovery_from_checkpoint_start),undo是受redo 保護的,因此可以從redo中恢復(臨時表undo除外,臨時表undo是不記錄redo的)。
在redo日誌應用完成後,初始化完成資料詞典子系統(dict_boot),隨後開始初始化事務子系統(trx_sys_init_at_db_start),undo 段的初始化即在這一步完成。
在初始化undo段時(trx_sys_init_at_db_start -> trx_rseg_array_init -> ... -> trx_undo_lists_init
),會根據每個回滾段page中的slot是否被使用來恢復對應的undo log,讀取其狀態資訊和型別等資訊,建立記憶體結構,並存放到每個回滾段的undo list上。
當初始化完成undo記憶體物件後,就要據此來恢復崩潰前的事務連結串列了(trx_lists_init_at_db_start),根據每個回滾段的insert_undo_list來恢復插入操作的事務(trx_resurrect_insert),根據update_undo_list來恢復更新事務(tex_resurrect_update),如果既存在插入又存在更新,則只恢復一個事務物件。另外除了恢復事務物件外,還要恢復表鎖及讀寫事務連結串列,從而恢復到崩潰之前的事務場景。
當從Undo恢復崩潰前活躍的事務物件後,會去開啟一個後臺執行緒來做事務回滾和清理操作(recv_recovery_rollback_active -> trx_rollback_or_clean_all_recovered),對於處於ACTIVE狀態的事務直接回滾,對於既不ACTIVE也非PREPARE狀態的事務,直接則認為其是提交的,直接釋放事務物件。但完成這一步後,理論上事務連結串列上只存在PREPARE狀態的事務。
隨後很快我們進入XA Recover階段,MySQL使用內部XA,即通過Binlog和InnoDB做XA恢復。在初始化完成引擎後,Server層會開始掃描最後一個Binlog檔案,蒐集其中記錄的XID(MYSQL_BIN_LOG::recover),然後和InnoDB層的事務XID做對比。如果XID已經存在於binlog中了,對應的事務需要提交;否則需要回滾事務。
Tips:為何只需要掃描最後一個binlog檔案就可以了? 因為在每次rotate到一個新的binlog檔案之前,總是要保證前一個binlog檔案中對應的事務都提交併且sync redo到磁碟了,也就是說,前一個binlog檔案中的事務在崩潰恢復時肯定是出於提交狀態的。
相關文章
- MySQL 引擎特性:InnoDB Buffer PoolMySql
- MySQL·引擎特性·InnoDBChangeBuffer介紹MySql
- MySQL • 原始碼分析 • mysql認證階段漫遊MySql原始碼
- MySQL·引擎特性·像NOSQL那樣使用MySQLMySql
- MySQL 引擎特性:InnoDB 同步機制MySql
- MySQL 引擎特性:InnoDB IO 子系統MySql
- MySQL 引擎特性:InnoDB崩潰恢復MySql
- 漫遊HttpURLConnectionHTTP
- 服務端漫遊服務端
- WiFi漫遊卡頓嚴重,如何有效改善實現無縫漫遊WiFi
- MySQL·引擎特性·基於InnoDB的物理複製MySql
- MySQL·引擎特性·InnoDB事務子系統介紹MySql
- 計算機系統漫遊計算機
- D3.js 漫遊指南JS
- CF 1500+ 題漫遊
- 談談MySQL InnoDB儲存引擎事務的ACID特性MySql儲存引擎
- 新特性:postgresql的vacuum漫談SQL
- Springboot漫遊日誌(1)Spring Boot
- 內網漫遊之SOCKS代理大結局內網
- 漫遊ZooKeeper nio通訊過程
- CameraPath實現簡單漫遊
- 漫遊MQ-socket 長連線MQ
- Vue2 原始碼漫遊(一)Vue原始碼
- Vue2 原始碼漫遊(二)Vue原始碼
- 遊標current of cur特性
- 【MySQL】MyRocks 漫談MySql
- 漫談mysql索引MySql索引
- MySQL引擎MySql
- MySQL之資料庫儲存引擎及事務ACID特性MySql資料庫儲存引擎
- Kubernetes 漫遊:理解 ConfigMap
- 漫遊jedis-RESP通訊實現
- 漫遊 servlet請求引數 %urldecodeServlet
- 安卓第四夜 概念漫遊(下)安卓
- 安卓第三夜 概念漫遊(上)安卓
- 從零開始,開啟屬於你的 RTE 漫遊之旅!丨漫遊指南 x 即將啟航
- 【MySQL】漫談死鎖MySql
- memory儲存引擎 /MySQL記憶體表的特性與使用介紹儲存引擎MySql記憶體
- 暑假旅遊新攻略,網路電話無漫遊打長途