本文是對整個Undo生命週期過程的闡述,代碼分析基於當前最新的MySQL5.7版本。本文也能夠做爲了解整個Undo模塊的代碼導讀。因爲涉及到的模塊衆多,所以部分細節並未深刻。php
Undo log是InnoDB MVCC事務特性的重要組成部分。當咱們對記錄作了變動操做時就會產生undo記錄,Undo記錄默認被記錄到系統表空間(ibdata)中,但從5.6開始,也可使用獨立的Undo 表空間。node
Undo記錄中存儲的是老版本數據,當一箇舊的事務須要讀取數據時,爲了能讀取到老版本的數據,須要順着undo鏈找到知足其可見性的記錄。當版本鏈很長時,一般能夠認爲這是個比較耗時的操做(例如bug#69812)。mysql
大多數對數據的變動操做包括INSERT/DELETE/UPDATE,其中INSERT操做在事務提交前只對當前事務可見,所以產生的Undo日誌能夠在事務提交後直接刪除(誰會對剛插入的數據有可見性需求呢!!),而對於UPDATE/DELETE則須要維護多版本信息,在InnoDB裏,UPDATE和DELETE操做產生的Undo日誌被歸成一類,即update_undo。sql
爲了保證事務併發操做時,在寫各自的undo log時不產生衝突,InnoDB採用回滾段的方式來維護undo log的併發寫入和持久化。回滾段其實是一種 Undo 文件組織方式,每一個回滾段又有多個undo log slot。具體的文件組織方式以下圖所示:數據庫
上圖展現了基本的Undo回滾段佈局結構,其中:數組
若是咱們使用獨立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記錄,在內存中維持了以下關鍵結構體對象:mvc
trx_sys->rseg_array
,數組大小爲128,分別對應不一樣的回滾段;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
。
普通回滾段的分配方式以下:
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
流程以下:
trx_undo_assign_undo
進行分配;trx_undo_assign_undo
進行分配。咱們來看看函數trx_undo_assign_undo的流程:
trx_undo_reuse_cached
,當知足某些條件時,事務提交時會將其擁有的trx_undo_t放到cached list上,這樣新的事務能夠重用這些undo 對象,而無需去掃描回滾段,尋找可用的slot,在後面的事務提交一節會介紹到);
trx_rseg_t::insert_undo_cached
上獲取,並修改頭部重用信息(trx_undo_insert_header_reuse)及預留XID空間(trx_undo_header_add_space_for_xid)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::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上
;
總的來講,undo header page主要包括以下信息:
入口函數: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內的偏移量,須要記錄到彙集索引記錄中。
入口函數: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狀態,這裏一樣經過Undo來實現的。
入口函數:trx_commit_low-->trx_write_serialisation_history
在該函數中,須要將該事務包含的Undo都設置爲完成狀態,先設置insert undo,再設置update undo(trx_undo_set_state_at_finish),完成狀態包含三種:
在確認狀態信息後,寫入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
)。
從上面的分析咱們能夠知道: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文件中的事務在崩潰恢復時確定是出於提交狀態的。