本文主要对 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格式