1 InnoDB Rollback Segment 1
1.1 Rollback Segment Allocation 1
1.2 Undo Segment Allocation 1
2 Undo Pages Deallocation 1
2.1.1 事务提交/回滚处理(Insert Undo Page回收) 2
2.1.2 Purge时事务的选择(Update Undo Page回收) 4
-
InnoDB Rollback Segment
-
Rollback Segment Allocation
多个rollback segment如何分配?其实很简单,round robin策略,在trx_assign_rseg函数中处理:
trx0trx.cc::trx_assign_rseg()
// 静态变量,记录上一次分配给事务的rollback segment
static ulint latest_rseg = 0;
I = latest_rseg++;
// max_undo_logs通过参数innodb_undo_logs控制,表示一个事务可以使用的
// rollback segment的数量;rollback segments总数量:innodb_rollback_segments
I %= max_undo_logs;
// 如果当前的rollback segment数量超过1,那么要求所有的undo records
// 不放在系统表空间中
do {
rseg = trx_sys->rseg_array[i];
I = I + 1;
} while(rseg == NULL || (rseg->space == 0 && trx_sys->rseg_array[1] != NULL))
-
Undo Segment Allocation
遍历事务指定的rollback segment的undo slot,如存在空闲slot,则分配给当前事务,如不存在空闲slot,则直接报错:DB_TOO_MANY_CONCURRENT_TRXS
-
Undo Pages Deallocation
Rollback Segments的个数是InnoDB系统启动时分配的,每个rollback segment占用的undo pages何时释放呢?是事务提交之后立即释放?或者说是后台慢慢回收,后台回收的依据是什么?
要回答以上的问题,首先必须对InnoDB的实现机制有所了解。InnoDB的二级索引的更新操作,不是直接对记录进行更新,而是标识旧记录为删除状态,然后新产生一条记录。删除操作也一样,标识记录为删除状态,并不实际删除记录。那么问题就来了:这些旧版本,标识为删除的记录何时真正删除,如何删除?
其实InnoDB是通过undo日志来进行旧版本的删除操作的,在InnoDB内部,这个操作被称之为purge操作,原来在srv_master_thread主线程中完成,后来进行优化,开辟了purge线程进行purge操作,并且可以设置purge线程的数量。purge操作每10s进行一次。
那么purge操作是如何进行的呢?最直观的想法就是:
-
按照事务的操作,进行purge
-
可以被purge的事务,一定是已经提交或者回滚的事务
-
可以被purge的事务,其所做的修改一定是可以被所有当前事务所见的事务
-
事务的insert操作是不需要被purge的,因为insert并不产生delete记录,因此
最好将事务的insert操作与update/delete操作分开(
想到了什么?每个事务,需要消耗两个undo slot,分别对应insert与update/delete操作,InnoDB已经帮我们想好了),insert操作的undo page,在事务提交之后能够直接回收
-
根据事务剩余的update/delete操作产生的undo,回收索引中的delete标识记录
-
最好能够
维护一个事务的提交顺序,先提交的事务先回收,然后再回收后提交的事务
其实,InnoDB大致就是这么做的,接下来我们可以看看相关部分的代码:
-
事务提交/回滚处理(Insert Undo Page回收)
trx0trx.c::trx_commit_off_kernel();
// 1. 处理update undo pages
// trx->no,事务提交id,标识事务提交的顺序
// 标识事务提交的no与标识事务创建的trx_id公用同一个id产生序列
// trx_no有以下几个意义:
// (1) 标识事务提交的顺序
// (2) 使得purge操作能够按照事务提交的顺序回收旧版本
// (3) trx_no与trx_id公用同一个id产生序列,那么在ReadView创建时,
// 所有小于trx_sys->max_trx_id的trx_no一定已经提交,而大于的trx_no
// 一定在当前ReadView创建之后才提交,在当前事务提交前,对应的trx_no
// 不能够被purge线程回收
trx->no = trx_sys_get_new_trx_no();
return (trx_sys->max_trx_id++);
// 获取update_undo对应的undo segment header page,设置其中的undo log
// segment状态(用于标识对应的事务状态)。在此,事务commit,对应的update
// undo segment header的状态应该是TRX_UNDO_TO_PURGE。
// 其他的状态包括:
// TRX_UNDO_ACTIVE:undo segment创建时设置为active,对应的事务是活跃事务
// TRX_UNDO_PREPARED:事务prepare时,设置为此状态
// TRX_UNDO_TO_FREE:事务commit时,insert undo segment设置此状态
// 在crash recovery时,根据状态信息来判断哪些是ACTIVE/PREPARE事务,并作
// 必要的回滚操作。
trx_undo_set_state_at_finish();
// InnoDB为了分配undo slot的性能考虑,做了一个小小的优化
// 事务提交时,事务对应的undo slot并不一定立即释放,而是cache起来
// 可以被cache起来的undo slot的条件如下:
// 1. 当前undo只占用一个undo page (cache起来代价较小,不至于消耗空间)
// 2. 当前undo对应的undo page,仍旧有足够的空闲空间,可以存放下一个
// undo的第一条undo头记录
if (undo->size == 1 &&
read(TRX_UNDO_PAGE_FREE) < TRX_UNDO_PAGE_REUSE_LIMIT)
undo->state = TRX_UNOD_CACHED;
trx_undo_update_cleanup(trx, update_hdr_page);
trx_purge_add_update_undo_to_history();
if (undo->state != TRX_UNDO_CACHED)
// 若当前的update undo slot没有被cache,那么则将undo slot清空
// 此undo slot可以被新的更新事务分配到
// 否则,当前update undo slot并不释放,而是放在update undo cache
// 中,下一次分配update undo slot可以快速从cache中获取
// 注意:cache undo slot并未从rseg的undo slots中摘除,page_no
// 仍旧被设置上
trx_rsegf_set_nth_undo(rseg_header, undo->id, FIL_NULL, mtr);
// 1.1 将update undo header page添加到
// rollback segment的历史事务链表中
flst_add_first(rseg_header + TRX_RSEG_HISTORY,
undo_header + TRX_UNDO_HISTORY_NODE);
trx_sys->rseg_history_len++;
// 1.2 同时将rollback segment中最早提交的事务的
// update undo header page单独保存一份,这个undo header page,
// 就是下一次purge的第一个page
if (rseg->last_page_no == FIL_NULL)
rseg->last_page_no = undo->hdr_page_no;
rseg->last_offset = undo->hdr_offset;
rseg->last_del_marks = undo->del_marks;
rseg->last_trx_no = trx_no;
// 最后,若history中保存的update undo为purge batch size的倍数
// 则唤醒purge线程操作,进行一次purge
if (!(trx_sys->rseg_hist_len % srv_purge_batch_size))
srv_wakr_purge_thread_if_not_active();
// 2. 接下来看看commit时如何处理insert操作对应的undo pages
// 简单了很多,直接删除即可。与我们前面提到的需求几乎一模一样
// 不需要保留insert undo header page,因为不需要purge insert操作
trx_undo_insert_cleanup();
// 将insert undo从回滚段的insert_undo_list中移除
UT_LIST_REMOVE(rseg->insert_undo_list, undo);
if (undo->state == TRX_UNDO_CACHED)
// 若判断为cache,则将insert undo加入到cache中,不需要释放page
UT_LIST_ADD_FIRST(resg->insert_undo_cached);
trx_undo_seg_free();
// 释放insert undo所占用的undo page
fseg_free_step();
// 将insert undo对应的undo slot指向的page设置为NULL,标识可用
trx_rseg_set_nth_undo(rseg_header, undo->id, FIL_NULL);
-
Commit时Undo的归属
一个事务,可能会分配Insert Undo与Update Undo,分别对应于事务的insert操作与update(delete)操作产生的undo。
Insert undo不会进入history_list,Update undo会进入history_list。
history_list长度无法限制,当有20个update undo进入后,就会唤醒purge线程,具体见上面的源码分析。而且后台也定期purge。靠这两个保证update undo的回收。
commit时,insert undo的归属如下:
commit时,update undo的归属如下:
-
Purge时事务的选择(Update Undo Page回收)
由于事务在提交时,包含update/delete操作的事务,已经按照事务提交的顺序链入rollback segment的历史事务链表,并且将最早提交的事务所对应的undo header page单独保存,因此purge时,只需要按照历史事务链表的顺序,purge所有历史事务产生的delete项即可,同时回收该历史事务对应的update undo pages:
trx_purge();
…
row0purge.c::row_purge();
//
trx_purge_fetch_next_rec();
trx_purge_choose_next_log();
// 遍历所有的rollback segment,取出其中最早提交的事务
while (rseg)
if (min_trx_no > rseg->last_trx_no)
min_trx_no = rseg->last_trx_no;
// purge操作的顺序,与按照事务操作的顺序一致
// 因此取出事务的第一条undo记录,而不是最后一条
trx_undo_get_first_rec();
// 设置purge系统结构的部分属性,包括:purge对应的rollback
// segment;purge对应的事务;purge已经处理了事务的多少undo;
purge_sys->rseg = min_rseg;
purge_sys->purge_trx_no = min_trx_no;
purge_sys->purge_undo_no = trx_undo_rec_get_undo_no(rec);
// 如果当前最老的历史事务,其提交的序列号要大于目前系统最老
// 未提及事务创建ReadView时获取的trx_no。则说明当前历史事务
// 还不能够被purge,需要等待系统最老事务提交之后才能被purge。
if (purge_sys->iter.trx_no >= purge_sys->view->low_limit_no)
return NULL;
// 读取当前历史事务的下一条undo记录
trx_purge_get_next_rec();
if (offset == 0)
// 若当前事务的undo记录已经purge完毕,则取rollback
// segment中的下一个提交事务,按照历史事务链表
trx_purge_rseg_get_next_history_log();
// 标识当前已经处理完一个undo header page(对应于事务
// 的update/delete操作),而不是一个undo page
// 早期单线程purge,系统默认一次purge 20个header page
purge_sys->n_pages_handled++;
// 从事务历史链表中获取下一个最早提交的事务
// 并且重新设置rollback segment中保存的最老事务信息
trx_purge_get_log_from_hist();
rseg->last_page_no = prev_log_addr.page;
// 在所有rollback segments中,重新选择最早提交的事务
trx_purge_choose_next_log();
// 解析得到的undo记录
row_purge_parse_undo_rec();
// 将标识为delete的索引项删除
row_purge_del_mark();
// 首先根据undo记录构造所有二级索引的搜索键,找到对应的
// delete记录,并真正删除。注意:
// 由于必须删除二级索引中的一项,因此undo中必须将所有二级
// 索引对应的列保存,哪怕这些列并未被更新(或者是删除操作)
row_purge_remove_sec_if_poss();
// 最后删除聚簇索引中的delete记录,如果delete操作,或者是
// update操作,但是修改主键,都会产生聚簇索引delete项
row_purge_remove_clust_if_poss();