change buffer(在 MySQL 5.6 之前叫 insert buffer,简称 ibuf )是 InnoDB 5.5 引入的一种优化策略,若二级索引页不在 buffer pool 中,则将针对二级索引页的操作暂时缓存起来,等到该页从磁盘读到 buffer pool 中时再批量的(batch)apply 这些操作,从而达到减少磁盘 I/O 的目的。具体一点就是:
为什么必须是二级索引页,不能是主键索引页?很简单,因为主键索引要保证唯一性的约束,如果把 insert id=1 缓存起来,再次有要 insert id=1 时再缓存起来,则等批量的 apply 时就会出错。change buffer 缓存的操作有三种:
change buffer 本质是一块写缓存,组织形式是 B-tree,存在于系统表空间中。其根节点位于系统表空间的第四页面(FSP_IBUF_TREE_ROOT_PAGE_NO)
其缓存的每一个操作叫做一个 entry,物理结构是(详见 ibuf_entry_build):
ibuf entry counter
ibuf entry counter 的存在是为了与 space_id 和 page_no 一起构成 entry 的主键,在 change buffer 里对于同一二级索引页的 entry,其 entry counter 是递增的
在 change buffer 中插入 entry 时,先定位到待插入的位置(btr_pcur_open):
0xFFFF 是最大的 entry counter(IBUF_REC_OFFSET_COUNTER 域为两个字节),所以 cursor 会定位到对应于同一二级索引页的具有最大 counter 的 entry,记为 max_counter。max_counter+1 即为待插入 entry 的 counter。 但在每一次 ibuf merge ,清空了该二级索引页的所有 entry 后,再插入针对该索引页的新的 ibuf entry,counter 又从 0 开始
如何得知一个操作是否会引起二级索引页的溢出?这需要我们追踪每个页的剩余空间。通过 ibuf bitmap page 来实现(Skywalker:InnoDB:Tablespace management(1)),ibuf bitmap page 用 4 bits 描述每个页:
IBUF_BITMAP_FREE 2 bits 的计算方式是:
UNIV_INLINEulint ibuf_index_page_calc_free_bits(ulint page_size, ulint max_ins_size) { // max_ins_size 是这个页中目前的全部剩余空间,IBUF_PAGE_SIZE_PER_FREE_SPACE 是 32 // page_size / IBUF_PAGE_SIZE_PER_FREE_SPACE 就是 512 bytes,可以看出这里只是粗糙 // 的统计 max_ins_size 是 512 bytes 的几倍,max_ins_size / 512: // - 大于 3(max_ins_size > 2048):IBUF_BITMAP_FREE 记录 3 // - 3(1536 < max_ins_size < 2048):IBUF_BITMAP_FREE 记录 3 // - 2(1024 < max_ins_size < 1536):IBUF_BITMAP_FREE 记录 2 // - 1(512 < max_ins_size < 1024):IBUF_BITMAP_FREE 记录 1 // - 0(max_ins_size < 512):IBUF_BITMAP_FREE 记录 0 n = max_ins_size / (page_size / IBUF_PAGE_SIZE_PER_FREE_SPACE);
if (n == 3) { n = 2; }
if (n > 3) { n = 3; }
return (n);}
在每次 insert / update / delete 后会更新 IBUF_BITMAP_FREE(ibuf_update_free_bits_if_full / ibuf_update_free_bits_low)。
只有在缓存 IBUF_OP_INSERT 时才需要防止页的分裂发生。这里涉及到一个函数 ibuf_get_volume_buffered。该函数用于计算对于特定的二级索引页( 设为 P1 ),change buffer 里缓存的操作 merge 完会导致此页增长的数据量是多少(affect the available space on the page),然后与该页的剩余空间做比较。因为 IBUF_BITMAP_FREE 最大就是 3,可见能缓存的操作最多只能占用 2048 bytes。 首先把 pcur 在 ibuf B-tree 中定位到缓存的关于 P1 的最大的 ibuf record。采用的依然是 B-tree 的函数,search tuple 是( P1 space id, P1 page no,0xFFFF),search mode 是 PAGE_CUR_LE,latch mode 是 BTR_MODIFY_PREV(x-latch 左兄弟节点和当前节点) 或 BTR_MODIFY_TREE(x-latch 左兄弟节点、当前节点和右兄弟节点)。定位完成 pcur 指向 之后从 pcur 开始:
放弃的意思是认为 change buffer 缓存的操作已经够多了,不能再缓存了。
ibuf_insert_low { ... /* Find out the volume of already buffered inserts for the same index page */ min_n_recs = 0; buffered = ibuf_get_volume_buffered(&pcur, page_id.space(), page_id.page_no(), op == IBUF_OP_DELETE? &min_n_recs: NULL, &mtr);
if (op == IBUF_OP_INSERT) { ulint bits = ibuf_bitmap_page_get_bits( bitmap_page, page_id, page_size, IBUF_BITMAP_FREE, &bitmap_mtr);
// 若 ibuf_get_volume_buffered 返回 UNIV_PAGE_SIZE,那么 if 里一定是 TRUE if (buffered + entry_size + page_dir_calc_reserved_space(1) > // 根据 IBUF_FREE_BITS 计算 Page 内的剩余空间,0 / 512 / 1024 / 2048 Bytes // change buffer 里缓存的针对 Page X 的最大容量不能超过 2048 Bytes ibuf_index_page_calc_free_from_bits(page_size, bits)) { /* Release the bitmap page latch early. */ ibuf_mtr_commit(&bitmap_mtr);
/* It may not fit */ do_merge = TRUE;
ibuf_get_merge_page_nos(FALSE, btr_pcur_get_rec(&pcur), &mtr, space_ids, page_nos, &n_stored); goto fail_exit; } }}
在上述正向 / 逆向遍历的过程中,对于每一个 ibuf entry,计算其对于二级索引页 Px 的可能占用的空间(ibuf_get_volume_buffered_count),并累加该值。计算方式依据 ibuf entry type,如果是 IBUF_OP_DELETE_MARK 则无影响,若是 IBUF_OP_INSERT 则会计算其可能占据的空间。
...ut_ad(ibuf_op == IBUF_OP_INSERT);
get_volume_comp : { dtuple_t *entry; ulint volume; dict_index_t *dummy_index; mem_heap_t *heap = mem_heap_create(500);
entry = ibuf_build_entry_from_ibuf_rec(mtr, rec, heap, &dummy_index);
volume = rec_get_converted_size(dummy_index, entry, 0);
ibuf_dummy_index_free(dummy_index); mem_heap_free(heap);
return (volume + page_dir_calc_reserved_space(1));}
这里需要计算 apply 完 change buffer 里的缓存,该索引页的剩余记录是多少。依然通过函数 ibuf_get_volume_buffered_count,以参数 n_rec 传递出来。
static ulint ibuf_get_volume_buffered(...lint *n_recs, /*!< in/out: minimum number of records on the page after the buffered changes have been applied, or NULL to disable the counting */)
计算方式就是遇到 IBUF_OP_DELETE 就把 n_recs --,遇到 IBUF_OP_INSERT或 IBUF_OP_DELETE_MARK 要注意:
Inserts can be done by updating a delete-marked record. Because delete-mark and insert operations can be pointing to the same records,we must not count duplicates.
如果要缓存的操作是 IBUF_OP_DELETE 且 n_recs < 2,说明这个操作可能导致页面变空,则不缓存到 ibuf 中。
当需要把一个关于二级索引页 P1 的操作缓存在 change buffer 中时,分为四步:
change buffer 会缓存三种操作:insert / delete mark / delete,前两种是用户事务的操作,最后一种是 purge 线程的操作。 首先我们需要知道在准备 purge 一个二级索引记录之前,均会判断这个记录是否可以被删除(row_purge_del_mark -> row_purge_remove_sec_if_poss)。拿到该二级索引记录中保存的主键,找到主键索引记录(row_search_index_entry),如果该主键索引记录存在某个 old version 满足下述三点,则该二级索引记录不能被删除(row_purge_poss_sec):
这三点表示主键索引还存在可能还需要被访问的旧版本,并且可能通过该二级索引记录检索,因此不能被 purge。
但如果使用 change buffer,上述的检查是不够的。比如这个场景:
在 merge 的时候 insert (1, A) 可能会重用原来被 delete mark 的记录 (1, A),即 "delete unmark";导致 purge 直接删除 (1, A) 后,相当于事务 insert (1, A) 丢失。
解决的办法很简单,当发现有 purge 线程准备缓存操作到 change buffer 中时,第 4 步中放弃缓存 insert。那么怎么发现 purge 线程准备缓存操作到 change buffer?首先在 buffer pool 中保存着这样一些 buf_block_t 结构体,它们不在 buffer chunk 中而是单独的内存区域,即 buf_pool->watch 数组,buf_block_t::state 初始状态是 BUF_BLOCK_POOL_WATCH。然后:
bpage->state = BUF_BLOCK_ZIP_PAGE;bpage->id = page_id;bpage->buf_fix_count = 1;
ut_d(bpage->in_page_hash = TRUE);HASH_INSERT(buf_page_t, hash, buf_pool->page_hash, page_id.fold(), bpage);
这样的话在第 4 步再去检查这个页是否被设置为 watch,即在 buf_pool->watch 数组中(buf_page_get_also_watch),如果是的话直接放弃。