本文主要对 InnoDB 的 Undo Log 进行介绍。
逻辑结构 物理结构 回滚段 InnoDB会为每一个读写事务分配回滚段,临时表有单独的回滚段。每一个回滚段内有两个重要的数据结构:
Undo slots, 可以理解为undo page的槽位,1个回滚段内部有1024个槽位,每个槽位可以指向FIL_NULL或者指向一个真实的undo page地址;回滚段为每个读写事务分配槽位时,只能分配指向FIL_NULL的槽位; 
TRX_RSEG_HISTORY,维护了一个undo log链表,与undo log中的TRX_UNDO_HISTORY_NODE关联,添加时加到头部; 
 
undo page 按照上文的描述,InnoDB为每1个读写事务分配一个undo slot槽位,对应的槽位会指向一个undo page。undo page可以分为两个部分:undo page header 和 undo log。undo page header是一个公共的区域,一个undo page内部可能会包含多个undo log,还可能出现undo page的复用。对于undo page header,主要的信息包括:
TRX_UNDO_PAGE_FREE:指向当前page的空闲位置 
TRX_UNDO_LAST_LOG:指向上一个undo log的开始位置 
TRX_UNDO_STATE:当前undo page状态 
TRX_UNDO_PAGE_LIST:维护一个undo page的链表,与TRX_UNDO_PAGE_NODE关联,添加时加到尾部 
 
undo log undo内部包括两个部分:undo log header 和 undo  log record,其中undo log header主要信息包括:
TRX_UNDO_LOG_START:当前undo log内容(不含header)的开始位置,undo->hdr_offset指向的是包含了header的开始位置,undo->top_offset指向的是log record(不含header)的开始位置 
 
生命周期 在介绍undo log的生命周期前,先回答下面几个问题。
1)什么时候申请回滚段?
A:这个问题前面已经提到,当事务升级为读写事务时,会自动的为事务分配回滚段。
2)undo log的写入过程?
A:读写事务分配好回滚段只是相当于做了一个占位,当有IDU操作发生时,才会开始记录undo。记录undo前,首先需要为事务绑定undo slot,寻找undo slot前,首先会去查看回滚段上是否有被cached的undo page,如果有的话,则可以直接拿来使用;如果没有则需要找到一个指向FIL_NULL的,然后生成一个新的undo page。
不管是cached的undo page还是新生成的undo page,在写入真实的undo log record前,会提前生成undo log header。然后根据具体的操作,记录相应的record。最后生成roll_ptr,最终绑定到数据行记录上。
3)undo log的回收过程?
A:对于undo log的回收,插入操作和非插入操作的处理流程不同,对于插入操作,可以在事务提交之后,直接清理掉undo log(或者进入cached队列);对于更新/删除操作,需要后台的purge线程进行删除,事务提交时仅将undo log加入到回滚段的history列表中(或者进入cached队列),并且会插入一个元素到purge_sys内的最小堆中。purge线程会异步的进行删除。
初始化 undo的初始化在innodb启动的时候进行,基本的入口在 srv_start() :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |--> srv_start |    |     |    |--> srv_undo_tablespaces_init   |    |    |--> srv_undo_tablespaces_create |    |    |    |--> srv_undo_tablespace_create |    |    |    |--> srv_undo_tablespace_open |    |--> trx_sys_create_sys_pages |    |--> trx_sys_init_at_db_start |    |    |--> trx_rsegs_init |    |    |    |--> trx_rseg_mem_create |    |--> trx_purge_sys_create |    | |    |     |    |--> recv_recovery_from_checkpoint_start |    |--> recv_recovery_from_checkpoint_finish |    |--> srv_undo_tablespaces_init   |    |    |--> srv_undo_tablespaces_open |    |    |    |--> srv_undo_tablespace_open_by_num |    |    |    |    |--> srv_undo_tablespace_open |    |--> trx_sys_init_at_db_start   |    |--> trx_purge_sys_create |    | |    |--> trx_rseg_adjust_rollback_segments |    |    |--> trx_rseg_add_rollback_segments |    |    |    |--> trx_rseg_create |    |    |    |--> trx_rseg_mem_create 
数据写入 主要代码入口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void  trx_assign_rseg_durable (trx_t  *trx)    ut_ad(trx->rsegs.m_redo.rseg == nullptr );   trx->rsegs.m_redo.rseg = srv_read_only_mode ? nullptr  : get_next_redo_rseg(); } |--> trx_set_rw_mode |    |--> trx_assign_rseg_durable |    |    |--> get_next_redo_rseg |    |    |    |--> get_next_redo_rseg_from_trx_sys   |    |    |    | |    |    |    |--> get_next_redo_rseg_from_undo_spaces   |    | |    |--> trx_sys_get_new_trx_id |    |--> trx_sys->rw_trx_ids.push_back |    |--> trx_sys->rw_trx_set.insert |    |--> UT_LIST_ADD_FIRST(trx_sys->rw_trx_list) 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 dberr_t  trx_undo_report_row_operation (...)    ...   err = trx_undo_assign_undo(trx, undo_ptr, TRX_UNDO_UPDATE);      ...   offset = trx_undo_page_report_modify(undo_page, trx, index, rec, offsets,                                        update, cmpl_info, clust_entry, &mtr);      ...   *roll_ptr = trx_undo_build_roll_ptr(op_type == TRX_UNDO_INSERT_OP,                                       undo_ptr->rseg->space_id, page_no, offset);      ... } dberr_t  trx_undo_assign_undo (     trx_t  *trx,                    trx_undo_ptr_t  *undo_ptr,      ulint type)                  ...   undo = trx_undo_reuse_cached(trx, rseg, type, trx->id, trx->xid, &mtr);        ...   err = trx_undo_create(trx, rseg, type, trx->id, trx->xid, &undo, &mtr); } static  MY_ATTRIBUTE ((warn_unused_result))  dberr_t     trx_undo_create (trx_t  *trx, trx_rseg_t  *rseg, ulint type, trx_id_t  trx_id,                      const  XID *xid, trx_undo_t  **undo, mtr_t  *mtr)    ...   rseg_header = trx_rsegf_get(rseg->space_id, rseg->page_no, rseg->page_size, mtr);   err = trx_undo_seg_create(rseg, rseg_header, type, &id, &undo_page, mtr);   ...   page_no = page_get_page_no(undo_page);   offset = trx_undo_header_create(undo_page, trx_id, mtr);   trx_undo_header_add_space_for_xid(undo_page, undo_page + offset, mtr);   *undo = trx_undo_mem_create(rseg, id, type, trx_id, xid, page_no, offset);   ... } 
从 MySQL 5.6 开始,MySQL 引入了独立的 undo 表空间,可以通过参数 innodb_undo_tablespaces 参数配置独立的 undo 表空间的个数,undo 可以不再通过 ibdata 进行管理(这也极大的减小了 ibdata 的大小)。从 MySQL 5.7 开始支持 truncate undo 表空间,进一步解决了 undo 空间膨胀后无法回收的问题。
在开启独立 undo 表空间的情况下,每个表空间内部包含了 128 个回滚段,读写事务在申请 undo 回滚段时,总是在不同的表空间交替进行分配。
当写操作(插入+更新)操作需要记录 undo 时,会通过已经分配的回滚段找到一个可用的 undo 。在前面的类图中可以看到,每个回滚段上对于不同类型的 undo 保存了两个列表:正在使用的列表和 cached 列表。在分配 undo page 时,会优先从 cached 列表中寻找,如果找不到的话再去分配一个新的 undo 。
前面也提到了,每个回滚段上都有一个 undo slots 数组,数组的长度为 1024,每个 slot 中保存的是一个 undo page 的地址,默认为空(FIL_NULL)。在分配一个新的 undo 时,首先需要找到一个为空的 slot,然后再去创建新的 page,并且将 slot 指向这个 page。结合代码分析一下:
1 2 3 4 5 6 7 8 9 10 11 12 |--> trx_undo_seg_create |    |--> trx_rsegf_undo_find_free   |    | |    |--> fsp_reserve_free_extents |    |--> fseg_create_general   |    |--> fil_space_release_free_extents |    | |    |--> trx_undo_page_init   |    |--> flst_init   |    |--> flst_add_last |    | |    |--> trx_rsegf_set_nth_undo   
事务提交 事务提交过程中的undo处理过程可以简化为以下逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 |--> ha_commit_trans |    |--> trx_prepare_for_mysql |    |    |--> trx_undo_set_state_at_prepare |    | |    |--> trx_commit_for_mysql |    |    |--> trx_write_serialisation_history |    |    |    |--> trx_serialisation_number_get |    |    |    |--> trx_undo_set_state_at_finish |    |    |    |--> trx_undo_update_clean |    |    | |    |    |--> trx_commit_in_memory |    |    |    |--> trx_undo_insert_clean 
其中,在trx_serialisation_number_get步骤中,会生成TrxUndoRsegs对象,插入到purge_sys->purge_queue中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if  ((redo_rseg != NULL  && redo_rseg->last_page_no == FIL_NULL) ||    (temp_rseg != NULL  && temp_rseg->last_page_no == FIL_NULL)) {   TrxUndoRsegs elem (trx->no)  ;   if  (redo_rseg != NULL  && redo_rseg->last_page_no == FIL_NULL) {     elem.push_back(redo_rseg);   }   if  (temp_rseg != NULL  && temp_rseg->last_page_no == FIL_NULL) {     elem.push_back(temp_rseg);   }   mutex_enter(&purge_sys->pq_mutex);   trx_sys_mutex_exit();   purge_sys->purge_queue->push(elem);   mutex_exit(&purge_sys->pq_mutex); } 
在 trx_undo_set_state_at_finish 步骤时,会判断当前 undo 的状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 page_t  *trx_undo_set_state_at_finish (...)    ...   undo_page = trx_undo_page_get(page_id_t (undo->space, undo->hdr_page_no),                                 undo->page_size, mtr);   seg_hdr = undo_page + TRX_UNDO_SEG_HDR;   page_hdr = undo_page + TRX_UNDO_PAGE_HDR;   if  (undo->size  == 1  && mach_read_from_2(page_hdr + TRX_UNDO_PAGE_FREE) <                              TRX_UNDO_PAGE_REUSE_LIMIT) {     state = TRX_UNDO_CACHED;   } else  if  (undo->type == TRX_UNDO_INSERT) {     state = TRX_UNDO_TO_FREE;   } else  {     state = TRX_UNDO_TO_PURGE;   }   undo->state = state;   mlog_write_ulint(seg_hdr + TRX_UNDO_STATE, state, MLOG_2BYTES, mtr);   return  (undo_page); } 
在trx_undo_update_clean步骤时,会将undo log加入到对应回滚段上的history列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void  trx_undo_update_cleanup (...)    ...   trx_purge_add_update_undo_to_history(       trx, undo_ptr, undo_page, update_rseg_history_len, n_added_logs, mtr);      ...   if  (undo->state == TRX_UNDO_CACHED) {     UT_LIST_ADD_FIRST(rseg->update_undo_cached, undo);     MONITOR_INC(MONITOR_NUM_UNDO_SLOT_CACHED);   } else  {     ut_ad(undo->state == TRX_UNDO_TO_PURGE);     trx_undo_mem_free(undo);   } } void  trx_purge_add_update_undo_to_history (...)    ...   if  (undo->state != TRX_UNDO_CACHED) {     ...     trx_rsegf_set_nth_undo(rseg_header, undo->id, FIL_NULL, mtr);     ...   }         flst_add_first(rseg_header + TRX_RSEG_HISTORY,                  undo_header + TRX_UNDO_HISTORY_NODE, mtr);      ...   if  (rseg->last_page_no == FIL_NULL) {     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;   } } 
后台purge 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |--> srv_purge_coordinator_thread |--> srv_worker_thread |--> trx_purge_attach_undo_recs  |    |--> trx_purge_fetch_next_rec  |    |    |--> trx_purge_choose_next_log  |    |    |    |--> purge_sys->rseg_iter->set_next()  |    |    |    |--> trx_purge_read_undo_rec  |    |    |    |    |--> trx_undo_get_first_rec |    |    | |    |    |--> trx_undo_build_roll_ptr |    |    |--> trx_purge_get_next_rec  |    |    |    |--> trx_undo_page_get_next_rec  |    |    |    |--> trx_undo_get_next_rec |    |    |    |    |--> trx_undo_page_get_next_rec |    |    |    |    |--> trx_undo_get_next_rec_from_next_page  |    |    |    |--> trx_purge_rseg_get_next_history_log  | |--> que_fork_scheduler_round_robin |--> srv_que_task_enqueue_low | |--> trx_purge_truncate  |    |--> trx_purge_truncate_history  |    |    |--> trx_purge_truncate_rseg_history  |--> row_purge |    |--> row_purge_parse_undo_rec |    |--> row_purge_record |    |    |--> row_purge_record_func |    |    |    |--> row_purge_del_mark |    |    |    |--> row_purge_upd_exist_or_extern 
undo 的 purge 由后台线程完成,后台线程分为两类:coordinator 线程和 worker 线程。coordinator 线程负责收集和分发 undo log,worker 线程负责实际的 purge 工作(coordinator 线程本身也会参与 purge)。
purge_sys 中维护了一个按照 trx_no 有序的小顶堆结构 purge_queue,在事务提交的过程中(commit阶段)会将事务对应的回滚段添加到 purge_queue 中。需要注意,并不是每次事务提交的时候都会将回滚段添加到 purge_queue 中,只有当 redo_rseg->last_page_no 为空时(即回滚段上之前不存在未 puge 的 undo),才会添加到 purge_queue 中。
若 redo_rseg->last_page_no 非空,则说明该回滚段上存在未 purg 的 undo,此时只会更新回滚段的 TRX_RSEG_HISTORY 列表,将新的 undo 添加到 TRX_RSEG_HISTORY 列表的头部。
coordinator 线程按照以下的逻辑收集 undo:
从 purge_queue 里面弹出一个元素 TrxUndoRsegs,m_trx_undo_rsegs 指向弹出的元素; 
继续从 purge_queue 弹出元素,若弹出元素的 trx_no 和 m_trx_undo_rsegs 的 trx_no 相同,则直接进行合并,否则退出循环; 
m_iter 指向 m_trx_undo_rsegs,开始遍历,purg_sys->rseg 指向待处理的回滚段; 
从待处理的回滚段中依次取 undo_record,若无法取到下一个 undo_record,则需要根据TRX_UNDO_HISTORY 的 next 指针,取下一个 trx 的 undo,并将 trx_rseg_t 重新 放入 purge_queue 中; 
m_iter 正常遍历,当遍历到 m_trx_undo_rsegs 尾部时,重复 1; 
 
参考文献 
http://mysql.taobao.org/monthly/2015/04/01/ 
http://mysql.taobao.org/monthly/2018/03/01/ 
http://mysql.taobao.org/monthly/2017/12/01/ 
 
附:undo格式