InnoDB Rollback Segment & Undo Page Deallocation实现源码分析

4月 23rd, 2012

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

 

  1. InnoDB Rollback Segment

    1. 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))

 

  1. Undo Segment Allocation

遍历事务指定的rollback segment的undo slot,如存在空闲slot,则分配给当前事务,如不存在空闲slot,则直接报错:DB_TOO_MANY_CONCURRENT_TRXS

  1. 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大致就是这么做的,接下来我们可以看看相关部分的代码:

  1. 事务提交/回滚处理(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);

  1. 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的归属如下:

  • 进入cached list,此时并未从rollback segment的undo slot中删除
  • free,直接释放,并且从rollback segment undo slot删除

commit时,update undo的归属如下:

  • 首先,必须进入history_list
  • 其次,若进入cache list,则不从Undo slot删除
  • 未进入cache list,从undo slot中删除

 

  1. 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();

  1. yanzongshuai
    6月 15th, 201516:18

    您好:
    “其实InnoDB是通过undo日志来进行旧版本的删除操作的,在InnoDB内部,这个操作被称之为purge操作,原来在srv_master_thread主线程中完成,后来进行优化,开辟了purge线程进行purge操作,并且可以设置purge线程的数量。purge操作每10s进行一次”
    开启了单独的Purge线程后,purge操作每10秒进行一次,我在srv_purge_coordinator_thread线程里没看到有关于多少秒就进行一次purge的操作,是不是后续版本就只在关机或者commit、rollback后会唤起purge线程进行purge操作?