InnoDB 之 UNDO LOG 介紹

語言: CN / TW / HK

undo log的組織形式

此部分是關於Undo log的組織形式的一個介紹;主要分為兩部分來對undo log的組織形式進行介紹:檔案結構和記憶體結構。在介紹這兩部分時,先從區域性出發,最後再給出各個部分的聯絡。

1. 檔案結構

首先,在MySQL5.6之前所有的undo log全部儲存在系統表空間中(ibdata1);但是從5.6開始也可以使用獨立表空間來儲存undo log。

當前版本InnoDB預設有兩個undo tablespace,也可以使用CREATE UNDO TABLESPACE語句動態新增,最大128個;每個undo tablespace至多可以有TRX_SYS_N_RSEGS(128)個回滾段。

1.1 Rollback Segment(回滾段)

InnoDB在undo tablespace中使用回滾段來組織undo log。同時為了保證事務的併發操作,在寫undo log時不產生衝突,InnoDB使用 回滾段 來維護undo log的併發寫入和持久化;而每個回滾段 又有多個undo log slot。通常通過Rollback Segment Header來管理回滾段,Rollback Segment Header通常在回滾段的第一個頁,具體結構如下:

  • Max Size:引數名為 TRX_RSEG_MAX_SIZE,回滾段可以有用的最大page數。
  • History Size:引數名為TRX_RSEG_HISTORY_SIZE,history list包含的page數。
  • History List Base Node:引數名為TRX_RSEG_HISTORY,history list的Base Node。
  • Rollback Segment FSEG Entry:引數名為TRX_RSEG_FSEG_HEADER,file segment的存放位置。
  • Undo Slots Dictionary:引數名為TRX_RSEG_UNDO_SLOTS,存放活躍事務的undo header page no。

Rollback Segment Header裡面最重要的兩部分就是 history listundo slot directory

其中history list把所有已經提交但還沒有被purge事務的undo log串聯起來,purge執行緒可以通過此list對沒有事務使用的undo log進行purge。

每個事務在需要記錄undo log時都會申請一個或兩個slot(insert/update分開),同時把事務的第一個undo page放入對應slot中;所以理論上InnoDB允許的最大事務併發數為128(undo tablespace) * 128(Rollback Segment) * 1024(TRX_RSEG_UNDO_SLOTS)。下面我們進一步介紹undo log在磁碟上如何記錄。

1.2 UndoPage

要想知道undo log如何記錄,我們先要搞清楚一個undo page具體內容,undo page一般分兩種情況:header page和normal page 。

header page除了normal page所包含的資訊,還包含一些undo segment資訊,後面會對undo segment進行詳細介紹。

我們下面先介紹一下undo header page的詳細分佈。

undo header page 是事務需要寫undo log時申請的第一個undo page;一個undo header page他同一時刻只隸屬於同一個活躍事務,但是一個undo header page上面的內容可能包含多個已經提交的事務和一個活躍事務。

undo normal page 是當活躍事務產生的undo record超過undo header page容量後,單獨再為此事務分配的undo page(參考函式trx_undo_add_page);此page只隸屬於一個事務,只包含undo page header不包含undo segment header。

1.3 Undo Page Header

每一個undo page都要有header,其中記錄了當前undo page的一些狀態資訊,具體內容如下:

  • Undo Page Type:引數名為TRX_UNDO_PAGE_TYPE,使用該page事務的型別,包含TRX_UNDO_INSERT,TRX_UNDO_UPDATE兩種。
  • Latest Log Record Offset:引數名為TRX_UNDO_PAGE_START,最新事務開始記錄undo log起始位置。
  • Free Space Offset:引數名為TRX_UNDO_PAGE_FREE,頁內空閒空間起始地址,在此之後可記錄undo log。
  • Undo Page List Node:引數名為TRX_UNDO_PAGE_NODE,undo page list節點,可以把同一個事務所用到的所有undo page雙向串聯起來。

1.4 Undo Segment Header

  • State:引數名為TRX_UNDO_STATE,undo segment的狀態,TRX_UNDO_ACTIVE等
  • Last Log Offset:引數名為TRX_UNDO_LAST_LOG,當前page最後一個undo log header的位置。
  • Undo Segment FSEG Entry:引數名為TRX_UNDO_FSEG_HEADER,segment對應的inode的(space_id,page_no,offset等)
  • Undo Segment Page List Base Node:引數名為TRX_UNDO_PAGE_LIST,undo page list的Base Node,對於同一個事務下的undo header page和undo normal page構成雙向連結串列。

上面只是介紹了一些undo log在檔案上的基本結構,下面我們繼續介紹記錄undo log時的檔案組織。

1.5 Undo Log Header

當事務開始記錄undo log時,先建立一個undo log header,當update/delete事務結束後,undo log header將會被加入到hisotry list中;insert事務的undo log會被立即釋放。

  • Transaction ID:引數名為TRX_UNDO_TRX_ID,事務id(事務開始的邏輯時間)
  • Transaction Number:引數名為TRX_UNDO_TRX_NO,事務no(事務結束的邏輯時間)
  • Delete Marks Flags:引數名為TRX_UNDO_DEL_MARKS,如果涉及到刪除記錄為TRUE
  • Log Start Offset:引數名為TRX_UNDO_LOG_START,事務中第一個undo record地址。
  • XID Flag:引數名為TRX_UNDO_XID_EXISTS,用於XID。
  • DDL Transaction Flag:引數名為TRX_UNDO_DICT_TRANS,是否是DDL事務
  • Table ID if DDL Transaction:引數名為TRX_UNDO_TABLE_ID,如果是DDL事務,記錄table id。
  • Next Undo Log Offset:引數名為TRX_UNDO_NEXT_LOG,當前頁的下一個undo log header位置
  • Prev Undo Log Offset:引數名為TRX_UNDO_PREV_LOG,當前頁的上一個undo log header位置
  • History List Node:引數名為TRX_UNDO_HISTORY_NODE,事務結束時放入history list的節點。

有關XID的內容暫時不介紹。

有了undo log header後,我們就可以記錄undo record了。

1.6 Undo Record

  • Previous Record Offset:儲存上一條record的位置。
  • Next Record Offset:儲存下一條record的位置。
  • Type + Extern Flag + Compilation Info:存undo record的type等資訊。

可以從上面圖中看出,undo record除了儲存這裡比較重要的幾個資訊,包含前後undo record位置,型別,undo no,table id等;而undo recod中具體儲存的內容我們等到《undo log的分配與記錄》中去介紹。

最後,我們通過下面這幅圖來了解undo log在檔案組織上的一個總覽。

2. 主要記憶體資料結構

為了方便管理和記錄undo log,在記憶體中有如下關鍵結構體物件:

  • undo::Tablespace:undo tablespace記憶體結構體,維護undo tablespace相關資訊,管理此tablespace中相關回滾段。
  • trx_rseg_t:回滾段的記憶體結構體,用於維護回滾段相關資訊。
  • trx_undo_t:undo log記憶體結構體,用於維護undo log type等資訊,便於對undo page進行維護和管理。
  • purge_pq_t:purge queue,對已經提交事務的undo log進行維護和回收。
  • trx_t:事務的記憶體結構體,對事務的資訊進行管理和維護。

我們重點對trx_rseg_t結構體的內容進行介紹。

trx_rseg_t 主要資料成員:

  • last_page_no:history list上此rseg最後一個沒有被purge的page no。
  • last offset:最後一個未被purge的undo log header 偏移。
  • last trx no:最後一個未被purge的事務no。
  • last_del_marks:最後一個未被purge的日誌需要被清理。

上面四個資料可以從trx_undo_t中獲取,參考trx_purge_add_update_undo_to_history函式。

  • trx_ref_count:被活躍事務引用的計數器;非0時,此回滾段所在的tablespace不可以被truncate。
  • update_undo_list:所有活躍的update事務的trx_undo_t物件儲存在此連結串列。
  • update_undo_cached:如果update事務提交時,此事務只使用了一個page並且此page剩餘空間大於1/4放入此連結串列;新update事務新申請undo log時優先從此連結串列分配。
  • insert_undo_list:所有活躍的insert事務的trx_undo_t物件儲存在此連結串列。
  • insert_undo_cached:如果insert事務提交時,此事務只使用了一個page並且此page剩餘空間大於1/4放入此連結串列;新insert事務新申請undo log時優先從此連結串列分配。

下面我們通過一副關係圖來介紹記憶體中各個關鍵結構體之間的關係;其中實線代表擁有該物件,虛線代表引用該物件。

undo log的分配與記錄

我們通過之前的介紹瞭解到;undo log在磁碟和記憶體中是如何組織的;從中瞭解到,回滾段不論在磁碟和記憶體中,都是一個非常關鍵的結構體;InnoDB儲存引擎通過回滾段來對undo log進行組織和管理,所以首先我們需要弄清楚回滾段是如何分配與使用的,之後再闡述undo log具體是如何記錄的。

1. 分配回滾段

當開啟一個事務的時候,需要預先為事務分配一個回滾段。

首先我們將事務分為兩大類:只讀事務與讀寫事務。分別從這兩大類事務來探討如何分配回滾段的。

只讀事務 :當事務涉及到對臨時表的讀寫時,我們需要為其分配一個回滾段對其產生的undo log record進行記錄,具體呼叫鏈路如下:

trx_assign_rseg_temp() -> get_next_temp_rseg() -> (trx_sys->tmp_rsegs)

trx_sys->tmp_rsegs 對應的臨時檔案為ibtmp1,一般來說有128個回滾段。

讀寫事務 :當一個事務被判定為讀寫模式時,會為其分配trx_id以及回滾段,具體呼叫鏈路如下

trx_assign_rseg_durable() -> get_next_redo_rseg()                                                  |                                                  ->get_next_redo_rseg_from_trx_sys() -> (trx_sys->rsegs)                                                  |                                                  ->get_next_redo_rseg_from_undo_spaces() -> (undo_space->rsegs())

當InnoDB沒有配置獨立undo表空間時,從trx_sys->rsegs為讀寫事務分配回滾段;否則則從 undo_spaces->rsegs()為其分配回滾段;InnoDB從MySQL 8.0.3開始,獨立表空間個數預設值從0改為2。

trx_sys->rsegs 對應的檔案為ibdata1,預設有128個回滾段。

undo_space->rsegs() 對應的檔案為undo_001,undo_002...,最多可有128個undo檔案,每個檔案預設128個回滾段。

具體從rsegs中分配時採用round-robin方式進行分配。

2. 使用回滾段

當發生資料變更時,我們需要使用undo log記錄下變更前的資料記錄;因此需要從回滾段分配中來分配一個undo slot來供事務記錄undo。

記錄undo的入口函式為trx_undo_report_row_operation,其大致流程如下:

  1. 判斷操作的表是否為臨時表;如果是臨時表,為其分配臨時表回滾段,否則使用普通回滾段。
  2. 根據事務型別,通過trx_undo_assign_undo為其分配trx_undo_t物件;之後事務產生的undo記錄在此物件中。
  3. 根據事務型別,通過trx_undo_page_report_insert/modify,來記錄insert/update事務產生的undo。

接著來看一下 trx_undo_assign_undo 函式流程:

  1. 首先嚐試通過trx_undo_reuse_cached() 來獲取可用的undo log物件。
  2. 對於INSERT型別的undo log,我們從rseg->insert_undo_cached連結串列上獲取undo log物件,並將其從連結串列上移除;之後通過trx_undo_insert_header_reuse()重新初始化undo page頭部資訊。
  3. 對於UPDATE/DELETE型別undo log,從rseg->update_undo_cached連結串列上獲取undo log物件,並將其從連結串列上移除;然後通過trx_undo_header_create()建立新的undo log header。
  4. 然後使用trx_undo_header_add_space_for_xid() 作用於上述undo log物件,預留XID儲存空間。
  5. 最後使用trx_undo_mem_init_for_reuse()初始化undo log物件相關資訊。
  6. 如果沒有快取的undo log物件,我們就需要使用trx_undo_create()從回滾段上分配一個空閒的undo slot,並分配一個undo page,對其初始化。
  7. 將已經分配好的undo log物件放入相關的連結串列中(rseg->insert_undo_list或rseg->update_undo_list)。
  8. 最後,如果這個事務時DDL操作,需要將undo_hdr_page(事務記錄undo log的第一個page)中的TRX_UNDO_DICT_TRANS置為TRUE.

undo header page結構參考之前的《undo log的組織形式》的內容

undo log最小的併發單元為undo slot,所以undo log支援最大的併發事務為:undo tablespace 數 * 回滾段數 * undo slot數。

3. undo log寫入

當分配完undo slot,初始化完undo log物件後,我們就可以記錄真正的undo log record;undo log record也分為一下兩種,insert undo log record與update undo record。

當資料庫需要修改某個資料記錄時,都會寫入一條update undo log record;當插入一條資料記錄時,會寫入一條insert undo log record。

對於insert undo log寫入的入口函式為trx_undo_page_report_insert()

  • Prev record offset (2) :本條record開始的位置。
  • Next record offset (2) :下一條record開始的位置。
  • Type (1) :標記undo log record的型別,此處一般為 TRX_UNDO_INSERT_REC.
  • Undo Number (1-11) :trx->undo_no,事務的第幾條undo。
  • Table ID (1-11) :聚集索引所對應的table id。
  • Unique Fields :唯一鍵值

對於update undo log寫入的入口函式為trx_undo_page_report_modify()

  • Prev record offset (2) :同上
  • Next record offset (2) :同上
  • Type+Extern Flag+Comp Info (1)
  • Type為undo log rec的型別,此處一般有三種:
  • TRX_UNDO_DEL_MARK_REC: 標記刪除操作,未修改任何列值;可能由普通刪除操作產生,也有可能由修改聚集索引產生,因為修改聚集索引操作被分拆為刪除+插入操作。
  • TRX_UNDO_UPD_DEL_REC: 更新一個已經被刪除的記錄;如某個記錄被刪除後,在很快插入一個相同的記錄;之前的記錄若未被purge,就可能重用該記錄所在位置。
  • TRX_UNDO_UPD_EXIST_REC: 更新一個未被標記刪除的記錄,也就是普通更新。
  • Extern Flag:是否有外部儲存列,以提示purge執行緒去清理外部儲存。
  • Comp Info:更新相關資訊,例如更新是否導致索引序發生變化。
  • Undo Number (1-11) :同上
  • Table ID (1-11) :同上
  • Info Bits (1) :是否標記刪除REC_INFO_DELETED_FLAG.
  • Data Trx ID (1-11) :修改舊記錄的事務ID。
  • Data Roll Ptr (1-11) :舊記錄的回滾指標。
  • Unique Fields :唯一鍵值
  • Update Get N Fields (1-5) : 更新的列數。
  • UPD Old Columns :發生更新時,舊記錄的內容。
  • Delete Fileds len (2) : 刪除的列數。
  • DEL Old Columns :發生刪除時,舊記錄的內容;發生刪除時並不總是記錄舊記錄,只有ord_part=1也就是說此欄位為n_uniq之一的欄位,才會記錄舊欄位。

在寫入過程中,可能出現undo page空間不足的情況;當出現這種情況,我們需要通過trx_undo_erase_page_end()來清除剛剛寫入的區域,然後通過trx_undo_add_page()申請一個新的undo page加入到undo page list,同時將undo->last_page_no指向新的undo page,最後重試寫入。

完成undo log record的寫入後,通過trx_undo_build_roll_ptr()構建新的回滾指標返回;通過回滾指標我們可以找到相關記錄的undo log record,從而構建出舊版本的資料;回滾指標將會記錄在聚集索引記錄中。

undo log的應用

我們通過之前的介紹已經瞭解到, undo log的組織方式與分配記錄;那麼後面我們繼續介紹undo log主要的應用是什麼。

undo log的應用主要有兩方面:

  1. 事務回滾,崩潰恢復;此功能主要滿足了事務的原子性,簡單的說就是要麼做完,要麼不做。因為資料庫在任何時候都可能發生宕機;包括停電,軟硬體bug等。那資料庫就需要保證不管發生任何情況,在重啟資料庫時都能恢復到一個一致性的狀態;這個一致性的狀態是指此時所有事務要麼處於提交,要麼處於未開始的狀態,不應該有事務處於執行了一半的狀態;所以我們可以通過undo log在資料庫重啟時把正在提交的事務完成提交,活躍的事務回滾,這樣就保證了事務的原子性,以此來讓資料庫恢復到一個一致性的狀態。
  2. 多版本併發控制(MVCC),此功能主要滿足了事務的隔離性,簡單的說就是不同活躍事務的資料互相可能是不可見的。因為如果兩個活躍的事務彼此可見,那麼一個事務將會看到另一個事務正在修改的資料,這樣會發生資料錯亂;所以我們可以藉助undo log記錄的歷史版本資料,來恢復出對於一個事務可見的資料,來滿足其讀取資料的請求。

我們接下來就詳細介紹上面兩個功能undo log是如何實現的。

1. 崩潰恢復

在InnoDB因為某些原因停止執行後;重啟InnoDB時,可能存在一個不一致的狀態,這個時候我們就需要把MySQL恢復到一個一致的狀態來保證資料庫的可用性。這個恢復過程主要分下面這麼幾步:

  1. 把最新的undo log從redo log中恢復出來,因為undo log是受redo log保護的。
  2. 根據最新的undo log構建出InnoDB崩潰前的狀態。
  3. 回滾那些還沒有提交的事務。

經過上面這三步後,InnoDB就可以恢復到一個一致的狀態,並且對外提供服務。

下面我們詳細的來介紹這三部分的具體過程:

1.1 undo log的恢復

因為undo log受到redo log的保護,所以我們只需要根據最新的redo log就可以把undo log恢復到最新的狀態;具體的呼叫過程如下:

recv_recovery_from_checkpoint_start()// 從最新的一個log checkpoint開始讀取redo log並應用。 | -> recv_recovery_begin() // 將redo log讀取到log buffer中,並將其parse到redo hash中 | -> recv_scan_log_recs() // 掃描 log buffer中的redo log,並將redo hash中的redo log應用 | -> recv_apply_hashed_log_recs() // 應用redo log到其對應的page上。 | ->recv_apply_log_rec()->recv_recover_page()->recv_parse_or_apply_log_rec_body() -> MLOG_UNDO_INSERT…

經過上述的流程之後,undo log就可以恢復到InnoDB崩潰前的最新的狀態;雖然undo log已經恢復到最新的狀態,但是InnoDB還沒有恢復到崩潰前的最新狀態;所以下一步我們就需要根據最新的undo log把InnoDB崩潰前的記憶體結構都恢復出來。

1.2 構建InnoDB崩潰前的狀態

構建InnoDB崩潰前的狀態,主要是恢復崩潰前最新事務系統的狀態;通過該狀態我們可以知道那些事務已經提交,那些事務還未提交,以及那些事務還未開始。

我們從前面兩章的介紹,回滾段不管在記憶體中還是在檔案中都是組織undo log的重要資料結構;所以我們首先需要把回滾段的記憶體結構恢復出來,然後根據記憶體中的回滾段,把活躍的事務恢復出來。其具體過程在函式trx_sys_init_at_db_start()中實現,其大致步驟如下:

  1. 通過trx_rsegs_init()掃描檔案中的回滾段結構,來把rseg的記憶體結構恢復出來。
  2. 通過trx_rseg_mem_create()把last_page_no,last_offset,last_trx_no,last_del_marks從檔案中讀取上來。
  3. 然後通過trx_undo_lists_init()把rseg的四個連結串列:insert_undo_list,insert_undo_cached,update_undo_list,update_undo_cached從磁碟上恢復出來。
  4. 在rseg記憶體結構恢復好之後,我們再通過trx_lists_init_at_db_start()把活躍的事務從rseg中恢復出來。
  5. 通過trx_resurrect_insert()恢復活躍的插入型別的事務。
  6. 通過trx_resurrect_update()恢復活躍的更新型別的事務。

至此,我們就已經把InnoDB崩潰前的記憶體和檔案狀態都已經恢復出來了;其實這個時候InnoDB已經可以對外提供服務了,(畢竟記憶體和檔案狀態都就緒後我們也就可以保持一致性了);那麼最後一步的事務回滾就可以交給後臺執行緒來慢慢做事務回滾,不影響主執行緒對外提供服務了。

1.3 事務回滾

事務需要回滾主要有兩種情況:

  1. 事務發生異常:如發生在崩潰恢復時;其活躍事務雖然被恢復出來,但是無法繼續,需要將其回滾。
  2. 事務被顯式回滾:如使用者開啟一個事務,執行完某些操作後需要將其回滾。

那麼在回滾時,我們就需要藉助undo log中的舊資料來把事務恢復到之前的狀態;其入口函式為row_undo_step();

其操作就是通過undo log來讀取舊的資料記錄,然後做逆向操作;主要分為下面這麼幾類:

  1. 對於標記刪除的記錄清理刪除標記。
  2. 對於in-place更新,將資料更新為老版本。
  3. 對於插入操作,刪除聚集索引記錄和二級索引記錄。
  4. 先通過row_undo_ins_remove_sec_rec()刪除二級索引記錄。
  5. 再通過row_undo_ins_remove_clust_rec()刪除聚集索引記錄。

2. 多版本併發控制(MVCC)

多版本併發控制簡單的說就是當前事務只能看見已經提交的資料記錄,看不到正在修改的資料記錄。所以我們只要弄清楚那些事務對於當前事務是已經提交的,那些事務對於當前事務是活躍的。

為了實現上述的功能我們先介紹幾個比較關鍵的概念:

trx::id :事務開始的邏輯時間,也叫事務ID,在事務開始時通過trx_start_low()分配。

trx::no :事務結束的邏輯時間,在事務結束的時候通過trx_commit_low()分配。

trx_sys::rw_trx_ids :當前活躍的事務ID;事務在開始時ID會被新增至此資料結構中;事務提交時ID會被從此資料結構中刪除。

在構造一條資料記錄時,我們除了在資料記錄中新增使用者自主新增的資料列,系統還會自動分配一些系統列,具體包括:

DATA_TRX_ID :修改過此行資料記錄的最新事務ID。

DATA_ROLL_PTR :指向這條資料記錄的上一個版本的指標,上一版資料在undo log中。

通過上面幾個資料結構的介紹,我們大概瞭解了一些基本概念;但是這對於一個事務來判斷那些事務對它是已經提交的,那些事務對它是活躍的還是遠遠不夠的;所以我們接下來介紹MVCC中最重要的一個數據結構:檢視,也就是read view。

2.1 檢視

每一個事務在讀取資料時都會被分配一個檢視,通過檢視就可以來判斷其他事務對資料記錄的可見性。下面我們來具體介紹一下檢視是如何運作的。

分配 :主要通過trx_assign_read_view()來給一個事務分配檢視;在事務的隔離級別是Consistent Snapshot 或 Read Repeatable時,事務開始時會給其分配;其他情況下當事務需要讀取資料時將會給其分配一個檢視。

回收 :事務結束時,會通過view_close()對其檢視進行回收。

幾個關鍵資料結構:

  1. m_low_limit_id:高水位,分配時取trx_sys::max_trx_id,也就是取當前還沒有被分配的事務ID。
  2. m_up_limit_id:低水位,如果m_ids不為空,取其最小值,否則取trx_sys::max_trx_id。
  3. m_ids:在此檢視初始化時,通過copy_trx_ids()從trx_sys::rw_trx_ids拷貝一份活躍事務ID(不包含當前事務ID)。

那麼有了上面這些資料我們就可以判斷那些事務對於此檢視是活躍的,那些事務對於此檢視是已經提交的。

那些事務對於此檢視是活躍的:

  1. trx_id > read_view::m_low_limit_id
  2. read_view::m_up_limit_id < trx_id < read_view::m_low_limit_id,並且 trx_id 屬於 trx_t::read_view::m_ids

如果給定一個trx_id滿足上面兩個條件其中之一,那麼這個事務對於此檢視就是活躍的。

那些事務對於此檢視是已經提交的:

  1. trx id < read_view::m_up_limit_id
  2. read_view::m_up_limit_id < trx id < read_view::m_low_limit_id,並且 trx id 不屬於 trx_t::read_view::m_ids

如果給定一個trx_id滿足上面兩個條件其中之一,那麼這個事務對於此檢視就是已經提交的。

2.2 資料可見性

通過上面的介紹,那麼一個事務就可以通過此事務的檢視來對資料記錄判斷可見性了。

具體是通過ReadView::changes_visible()來判斷可見性的,具體如下:

假設一個事務為T,trx_id為記錄R中的DATA_TRX_ID:

  1. trx_id > read_view::m_low_limit_id,T 不可見 R
  2. trx_id < read_view::m_up_limit_id,T 可見 R
  3. read_view::m_up_limit_id =< trx_id <= read_view::m_low_limit_id時,如果trx_id 屬於 trx_t::read_view::m_ids 時,T 不可見 R。否則可見 R

如果T對R不可見,就需要R中的DATA_ROLL_PTR來構造出上一個資料頁版本,直至記錄可見。

我們通過下面一個例子來說明可見性。

undo log 的清理

我們通過之前的系列文章已經瞭解到undo log在磁碟和記憶體中是如何組織的;undo log是如何分配的;以及undo log是如何使用的。那麼undo log會一直記錄下去麼?當然不是,有些undo log如果沒用的話是會被回收清理的。

那麼下面這將會介紹那些undo log可以清理,以及undo log是怎麼進行清理的。

1. 幾個關鍵的資料結構

在介紹undo log 清理之前,先介紹幾個關鍵的資料結構;這幾個資料結構對於undo log的清理實現是至關重要的。

trx_sys->serialisation_list : 裡面存放的是正在提交的事務,按照trx_t::no有序的排列;事務會在開始提交時通過 trx_serialisation_number_get() 新增至該資料結構,事務結束提交時通過trx_erase_lists()將該事務從該資料結構中移除。

read_view::m_low_limit_no :擁有該read_view的物件,對於trx_t::no小於read_view::m_low_limit_no的undo log都不在需要;該變數的取值時trx_sys->serialisation_list中最早的一個事務的trx_t::no;因為trx_sys->serialisation_list內有序存放的正在提交的事務,如果一個事務的trx_t::no比該數值還小,那麼這個事務一定已經提交了。

TRX_RSEG_HISTORY與TRX_UNDO_HISTORY_NODE :這兩個值我們之前在《undolog的組織形式》裡簡單介紹過,這兩個值共同將回滾段中的history list組織起來;在事務提交時,如果是update/delete型別的undo log,將其undo log header以頭插法的方式通過trx_purge_add_update_undo_to_history()加入到該回滾段的history list中,如果是insert型別的undo log其空間會被當場釋放,這是因為insert記錄沒有舊的版本;因此history list中的undo log header是以trx_t::no降序排列的,這裡需要注意一下: history list裡面的節點是undo log header 。下面我們通過一幅圖來具體說明下磁碟上history list的結構。

2. 那些undo log可以清理?

對於一個事務來說,早於read_view::m_low_limit_no的undo log都不需要訪問了;那麼如果存在一個read view,其read_view::m_low_limit_no比所有read view的m_low_limit_no都要小,那麼小於此read_view::m_low_limit_no的undo log就不在被所有活躍事務所需要了,那麼這些undo log就可以清理了。

在read_view初始化時,會使用頭插法通過view_open()插入到一個全域性檢視連結串列(MVCC::m_views)中,在事務結束時通過view_close()會從全域性檢視連結串列中將此read view移除;因為是順序插入,所以此連結串列中最後一個還沒有close的檢視就可以看做是最老的一個檢視;小於此檢視的undo log可以被清理,一般將此檢視賦值給purge_sys::view。

現在我們已經可以決定那些undo log是可以被清理的,那麼下一步我們還需要找到具體那些undo log可以清理。

在事務提交時,此事務對應的回滾段會通過trx_serialisation_number_get()加入到purge_sys::purge_queue中。

purge_sys::purge_queue是一個以回滾段中第一個提交事務的trx_t::no為key的優先順序佇列。

如此一來,從purge_sys::purge_queue取出的回滾段中一定包含最老提交的事務,將此事務的trx_t::no與purge_sys::view對比,即可判斷出此事務相關的undo log是否可以被清理。

purge_sys::purge_queue的詳細資訊如下圖:

3. undo log怎麼清理?

解決了那些undo log可以清理的問題後,下面接著繼續看undo log怎麼進行清理的問題。

當放入history list的undo log且不會再被訪問時,需要進行清理操作,另外資料頁上面的標記刪除的操作也需要清理掉,有一些purge執行緒負責這些操作,去入口函式為srv_do_purge() -> trx_purge(),其大致流程如下:

  1. 通過trx_sys->mvcc->clone_oldest_view()獲取最老的檢視複製給purge_sys::view,方便之後真正purge undo log時判斷其是否不會再被訪問到了。
  2. 通過trx_purge_attach_undo_recs()獲取需要被purge的undo log,其大致流程如下:
  3. 通過trx_purge_fetch_next_rec()迴圈獲取可以被purge的undo log,預設一次最多獲取300個undo log record,可以通過innodb_purge_batch_size來調整。
  4. 迴圈獲取可以被purge的undo log record大致流程如下:
  5. 從purge_sys::purge_queue取出第一個回滾段,從其history list上讀取最老還未被purge的事務的undo log header。
  6. 從此undo log header依次讀取undo log record。
  7. 讀取完畢後,重新統計此回滾段最老還未被purge的事務的位點,然後重新放入purge_sys::purge_queue;最後回到第一步。
  8. 將這些undo log分發給purge工作執行緒,purge工作執行緒的入口函式為row_purge_step()->row_purge()->row_purge_record()。

這裡purge undo log record時主要分為兩種情況:清理TRX_UNDO_DEL_MARK_REC記錄或者清理TRX_UNDO_UPD_EXIST_REC記錄

  1. 清理TRX_UNDO_DEL_MARK_REC型別的記錄,需要通過row_purge_del_mark()將所有的聚集索引與二級索引記錄都清除掉。
  2. 清理TRX_UNDO_UPD_EXIST_REC型別的記錄,需要通過row_purge_upd_exist_or_extern()將舊的二級索引清理掉。
  3. 通過trx_purge_truncate()來對history list進行清理,其大致流程如下:
  4. 遍歷所有回滾段,並通過trx_purge_truncate_rseg_history()對回滾段中的history list進行清理,其大致流程如下:
  5. 將history list最後一個事務的undo log header讀取出來。
  6. 判斷此undo log是否已經被purge,如果已經被purge則繼續;如果沒有被purge則退出。
  7. 將此事務所有的undo log釋放,並從history list上刪除;會到第一步。
  8. 在這之後,如果發現某些undo tablespace空間佔用過大,被標記需要通過trx_purge_truncate_marked_undo()進行對其truncate,其大致流程如下:
  9. 建立一個undo_trunc.log的標記檔案,來表明當前undo tablespace正在進行truncate;這是為了保證在truncate中間發生重啟時可以順利重建此undo tablespace。
  10. 通過trx_undo_truncate_tablespace()介面來對其檔案做真正的truncate。
  11. 刪除undo_trunc.log標記檔案,表明undo tablespace的truncate已經完成。

注意:當一個undo tablespace被標記為需要truncate時,不會再有事務從此undo tablespace分配回滾段,而且進行truncate時必須保證該undo tablespace上所有的undo log都已經被purge。

4. 最後

通過上面的介紹,我們知道了undo log那些記錄可以被清理以及是怎麼清理的,但是清理undo log過程中還有很多繁雜的細節;比如清理索引時涉及到對B樹的操作,以及舊版本資料的構建,XA事務,BLOB等等;這類內容暫時略過後面有機會繼續介紹。

參考內容

原文連結

本文為阿里雲原創內容,未經允許不得轉載。