何时进行页面分裂
插入操作时首先调用乐观插入函数btr_cur_optimistic_insert,若发现此次空间不足,进行悲观插入,函数btr_cur_pessimistic_insert,在这里会触发页面分裂。
dberr_t btr_cur_optimistic_insert(...) {
...
if (leaf && page_size.is_compressed() &&
(page_get_data_size(page) + rec_size >=
dict_index_zip_pad_optimal_page_size(index)))
{
...
fail:
err = DB_FAIL;
// 接下来要进入page split,提前发起对兄弟页面的预取 // 这样split的时候兄弟页面很可能已经在buffer内了,提升split性能 if (page_is_leaf(page)) {
btr_cur_prefetch_siblings(block);
}
fail_err:
...
return (err);
}
ulint max_size = page_get_max_insert_size_after_reorganize(page, 1);
// 如果page内空间不足,此时需要split,跳转进入fail,返回DB_FAIL if (page_has_garbage(page)) {
if ((max_size < rec_size || max_size < BTR_CUR_PAGE_REORGANIZE_LIMIT) &&
page_get_n_recs(page) > 1 &&
page_get_max_insert_size(page, 1) < rec_size) {
goto fail;
}
} else if (max_size < rec_size) {
goto fail;
}
}
dberr_t row_ins_clust_index_entry_low()
{
...
if (mode != BTR_MODIFY_TREE) {
...
} else {
...
// 先执行乐观插入,如果返回DB_FAIL,则执行悲观插入 // 悲观插入涉及到页面分裂 err = btr_cur_optimistic_insert(flags, cursor, &offsets, &offsets_heap,
entry, &insert_rec, &big_rec, n_ext, thr,
&mtr);
if (err == DB_FAIL) {
err = btr_cur_pessimistic_insert(flags, cursor, &offsets, &offsets_heap,
entry, &insert_rec, &big_rec, n_ext,
thr, &mtr);
}
悲观插入因为涉及到页面分裂,逻辑较为复杂,我们尝试尽量将其简单化描述:
dberr_t btr_cur_pessimistic_insert(...)
{
...
// 首先预留空间以保证本次split操作成功 // 至于预留多少,没有搞清楚 if (!(flags & BTR_NO_UNDO_LOG_FLAG) || index->table->is_intrinsic()) {
ulint n_extents = cursor->tree_height / 16 + 3;
...
success = fsp_reserve_free_extents(&n_reserved, index->space, n_extents,
FSP_NORMAL, mtr);
if (!success) {
return (DB_OUT_OF_FILE_SPACE);
}
}
// 如果是需要分裂的leaf page是root page if (dict_index_get_page(index) ==
btr_cur_get_block(cursor)->page.id.page_no())
{
*rec = btr_root_raise_and_insert(flags, cursor, offsets, heap, entry, n_ext,
mtr);
} else {
// 普通页面分裂 *rec = btr_page_split_and_insert(flags, cursor, offsets, heap, entry, n_ext,
mtr);
}
}
我们先来看如何分裂root page。
root页面分裂
root page分裂的过程是:申请一个新page,将root的记录,转移到新page中;重建旧root并将其作为新的root;然后对新page进行节点分裂,新root变为索引节点,其中插入记录指向新page。而新page的分裂则是走下面描述的普通页面分裂流程。
rec_t *btr_root_raise_and_insert(...)
{
...
// 创建新page, 其B-Tree层高等于root page的层高 level = btr_page_get_level(root, mtr);
new_block = btr_page_alloc(index, 0, FSP_NO_DIR, level, mtr, mtr);
new_page = buf_block_get_frame(new_block);
new_page_zip = buf_block_get_page_zip(new_block);
btr_page_create(new_block, new_page_zip, index, level, mtr);
btr_page_set_next(new_page, new_page_zip, FIL_NULL, mtr);
btr_page_set_prev(new_page, new_page_zip, FIL_NULL, mtr);
// 从root page中拷贝所有记录至新创建page,代码省略 page_zip_copy_recs(new_page_zip, new_page, root_page_zip, root, index, mtr);
...
// 新page已经拷贝了原root page的所有行记录 // rec指向新page的非infimum的第一个行记录 // 后面构建的索引记录其实就是该rec rec = page_rec_get_next(page_get_infimum_rec(new_page));
// 构建索引记录指向新page // 索引记录是一个tuple对象,其中除记录rec的索引外,还有新page的page_no // 该记录指向新page的第一个非infimum行记录 new_page_no = new_block->page.id.page_no();
if (dict_index_is_spatial(index)) {
...
} else {
node_ptr = dict_index_build_node_ptr(index, rec, new_page_no, *heap, level);
}
...
// 现在,root page可以被清空并插入上面创建的索引记录以指向其孩子页面 btr_page_empty(root_block, root_page_zip, index, level + 1, mtr);
btr_page_set_next(root, root_page_zip, FIL_NULL, mtr);
btr_page_set_prev(root, root_page_zip, FIL_NULL, mtr);
page_cursor = btr_cur_get_page_cur(cursor);
page_cur_set_before_first(root_block, page_cursor);
// 插入至新的root page node_ptr_rec = page_cur_tuple_insert(page_cursor, node_ptr, index, offsets,
heap, 0, mtr);
...
// 在新page中定位到待插入的tuple位置,记录在page_cursor page_cur_search(new_block, index, tuple, page_cursor);
// 接下来在新page中开始分裂并插入目标记录 // 走普通页面分裂流程,在下面详细描述 if (dict_index_is_spatial(index)) {
...
} else {
return (btr_page_split_and_insert(flags, cursor, offsets, heap, tuple,
n_ext, mtr));
}
}
举例说明,假如表结构还是如之前创建的那样:
mysql> CREATE TABLE t (
-> a INT NOT NULL PRIMARY KEY,
-> b VARCHAR(3500)
-> ) ENGINE=INNODB CHARSET=LATIN1 ROW_FORMAT=COMPACT;
可以知道该表页面最多可插入4条记录,再插入第五条时page便会产生分裂,假如
mysql> INSERT INTO t SELECT 10,REPEAT('a',3500);
mysql> INSERT INTO t SELECT 20,REPEAT('a',3500);
mysql> INSERT INTO t SELECT 30,REPEAT('a',3500);
mysql> INSERT INTO t SELECT 40,REPEAT('a',3500);
使用分析工具看到此时只有一个root page,且其中有以下记录:
************ FILE_PAGE_HEADER ****************
0: page_crc : 0x38e917fe
4: page_no : 4
8: page_version : 4294967295
12: space_version : 4294967295
16: page_lsn : 3363988429
24: page_type : FIL_PAGE_INDEX
26: page_flush_lsn: 0
34: space_id : 9
************ FILE_PAGE_HEADER ****************
42: page_format : COMPACT
38: n_dir_slots : 2
40: heap_top : 14220
42: heap_number : 0x8006
44: free : 0
46: deleted_bytes: 0
48: last_insert : 10703
50: direction : PAGE_RIGHT
52: n_direction : 3
54: n_records : 4
56: max_trx_id : 0
64: level : 0
66: index_id : 153
74: btr_seg_leaf : Space: 9, Page: 2, Offset: 626
84: btr_seg_top : Space: 9, Page: 2, Offset: 434
99: records[ 0]: n_owned: 1, heap_no: 0, rec_type: REC_INFIMUM, next_rec: 29
128: records[ 1]: n_owned: 0, heap_no: 16, rec_type: REC_DATA, next_rec: 3525
3653: records[ 2]: n_owned: 0, heap_no: 16, rec_type: REC_DATA, next_rec: 3525
7178: records[ 3]: n_owned: 0, heap_no: 32, rec_type: REC_DATA, next_rec: 3525
10703: records[ 4]: n_owned: 0, heap_no: 32, rec_type: REC_DATA, next_rec: 54945
112: records[ 5]: n_owned: 5, heap_no: 1, rec_type: REC_SUPREMUM, next_rec: 0
16372: slots[ 1]: 112
16374: slots[ 0]: 99
假如此时插入第五条记录:
mysql> INSERT INTO t SELECT 50,REPEAT('a',3500);
此时会进入root页面分裂,创建一个新page,将root page中已有的行记录全部拷贝进去,然后构建一个指针记录,指向新page的第一条记录,然后将指针记录插入新root page,于是现在的结构如下图所示:
原本的root page no为4,分配了一个新的page 5来存储了4中所有记录并清空4,且在4中插入了一条指针记录,其key为10,page_no为5,指向page 5的第一条记录。
接下来只需要针对5做split就可以了,而这只是一次普通页面分裂。
普通页面分裂
普通页面分裂的实现位于函数btr_page_split_and_insert,主要步骤如下:从要分裂的page 中, 找到要split 的record, split 的时候要保证split 的位置是record 的边界
分配新的索引页
分别计算page, 和new_page 的边界record
在父节点添加新的索引页的索引项, 如果父节点空间不足, 触发父节点分裂操作
连接当前索引页, 当前索引页prev_page, next_page, father_page, 新创建的 page. 当前的连接顺序是先连接父节点, 然后是prev_page/next_page, 最后是 page 和 new_page
将当前索引页上的部分record 移动到新索引页
SMO 操作已经结束, 计算本次insert 要插入的page 位置
进行insert 操作, 如果insert 失败, 通过reorgination page 重新尝试插入
rec_t *btr_page_split_and_insert(...)
{
insert_left = FALSE;
// 这里有个优化: // 进行分裂时,如果定位的cursor是当前页的尾部,先试图向右兄弟页插入 // 如果插入失败,再进行分裂,可减少分裂次数。 rec = btr_insert_into_right_sibling(flags, cursor, offsets, *heap, tuple,
n_ext, mtr);
if (rec != NULL) {
return (rec);
}
// 以下的复杂逻辑用来判断要page的split record位置,后面专门描述 // 如果上面的两种情况都不满足 // 1. 如果page中的用户行记录数大于1,那就取page的中间记录作为split_rec // 2. 否则: // 2.1 如果要插入的tuple小于page的第一个记录(其实也是唯一记录),那么从该唯一记录处开始split // 2.2 否则,split_rec设置为null if (n_iterations > 0) {
...
} else if (btr_page_get_split_rec_to_right(cursor, &split_rec)) {
direction = FSP_UP;
hint_page_no = page_no + 1;
} else if (btr_page_get_split_rec_to_left(cursor, &split_rec)) {
direction = FSP_DOWN;
hint_page_no = page_no - 1;
} else {
direction = FSP_UP;
hint_page_no = page_no + 1;
if (page_get_n_recs(page) > 1) {
split_rec = page_get_middle_rec(page);
} else if (btr_page_tuple_smaller(cursor, tuple, offsets, n_uniq, heap)) {
split_rec = page_rec_get_next(page_get_infimum_rec(page));
} else {
split_rec = NULL;
}
}
// 创建一个新索引page,以容纳老page的部分record new_block = btr_page_alloc(cursor->index, hint_page_no, direction,
btr_page_get_level(page, mtr), mtr, mtr);
new_page = buf_block_get_frame(new_block);
new_page_zip = buf_block_get_page_zip(new_block);
btr_page_create(new_block, new_page_zip, cursor->index,
btr_page_get_level(page, mtr), mtr);
// 以下主要计算first_rec和move_limit,以便于后面的move // 上面的所有都是为了计算这两个变量 // 如果split_rec不为null,那么first_rec和move_limit都是split_rec if (split_rec) {
first_rec = move_limit = split_rec;
*offsets =
rec_get_offsets(split_rec, cursor->index, *offsets, n_uniq, heap);
insert_left = cmp_dtuple_rec(tuple, split_rec, cursor->index, *offsets) < 0;
if (!insert_left && new_page_zip && n_iterations > 0) {
split_rec = NULL;
goto insert_empty;
}
} else if (insert_left) {
first_rec = page_rec_get_next(page_get_infimum_rec(page));
move_limit = page_rec_get_next(btr_cur_get_rec(cursor));
} else {
insert_empty:
buf = UT_NEW_ARRAY_NOKEY(
byte, rec_get_converted_size(cursor->index, tuple, n_ext));
first_rec = rec_convert_dtuple_to_rec(buf, cursor->index, tuple, n_ext);
move_limit = page_rec_get_next(btr_cur_get_rec(cursor));
}
// 修改B-Tree:因为生成了一个新page,因而需要在父page中增加索引记录 // 而这可能又会产生新的页面分裂 btr_attach_half_pages(flags, cursor->index, block, first_rec, new_block,
direction, mtr);
// 分裂原页面的行记录至两个页面 // 分裂的原理: // 1. 首先将原页面的内容全部拷贝至新页面 // 2. 删除原页面的部分用户行记录 // 3. 删除新页面的部分用户行记录 if (direction == FSP_DOWN) {
...
} else {
...
}
// B-Tree结构修改和物理记录搬迁都已完成,接下来将目标记录插入至合适位置 if (insert_left) {
insert_block = left_block;
} else {
insert_block = right_block;
}
// 确定目标记录的插入位置并尝试插入,如果成功则退出,否则整理page然后重试 page_cursor = btr_cur_get_page_cur(cursor);
page_cur_search(insert_block, cursor->index, tuple, page_cursor);
rec = page_cur_tuple_insert(page_cursor, tuple, cursor->index, offsets, heap,
n_ext, mtr);
...
}
仍然以上面的例子来说明,split前B-Tree结构如下:
第一步确定split_rec的位置,由于page 5是从老root page拷贝而来,但LAST_INSERT信息则是没有继承自原page,因而split_rec取的是mid_rec,为rec-3,因而也就确认了first_rec和move_limit也为rec-3。
第二步是创建新page,且page_no为6,并根据split_rec在父页面中为其创建一个新的指针记录,指向page 6的第一个记录rec-3。
第三步是真正的页面分裂,先将page 5的所有用户记录拷贝至page 6,而由于插入方向为FSP_UP,因而在page 6中就是删除rec-3以前的所有记录(不包括rec-3),而在page 5中则是删除rec-3以后的所有用户记录(包括rec-3)。
第四步便是将目标记录插入至特定页面,这里是插入至page 6,此时B-Tree结构如下:
split record计算
在前面普通页面分裂时会涉及到计算split record,即从该记录开始的所有行记录均会被移动至新页面。这部分逻辑牵涉面较多,特拎出来单独分析。
rec_t *btr_page_split_and_insert(...)
{
insert_left = FALSE;
...
// 计算规则:
// 如果insert_point位于PAGE_LAST_INSERT前(也即插入模式是顺序插入) // 1. 如果insert_point的下一个记录是supremum rec,那从该要插入的record处开始split // 2. 如果如果insert_point的下下一个记录是supremum rec,那同情况1 // 3. 如果上述1/2都不成立,意味着insert point后至少有2条用户记录,从insert point的下下一条记录处开 // 始分裂(为什么这么做?) // case-1: // ---------------------------------------------------- // | infimum | rec-1 | ... | PAGE_LAST_INSERT | ..... | // ---------------------------------------------------- // ^ // | --------> split rec // insert point // case-2: // ----------------------------------------- // | infimum | PAGE_LAST_INSERT | supremum | // ----------------------------------------- // ^ // |--------> split rec // insert point // case-3: // ---------------------------------------------- // | infimum | rec-1 | PAGE_LAST_INSERT | ..... | // ---------------------------------------------- // ^ ^ // | |--------> split rec // insert point // 如果上面的两种情况都不满足 // 1. 如果page中的用户行记录数大于1,那就取page的中间记录作为split_rec // 2. 否则: // 2.1 如果要插入的tuple小于page的第一个记录(其实也是唯一记录),那么从该唯一记录处开始split // 2.2 否则,split_rec设置为null if (n_iterations > 0) {
...
} else if (btr_page_get_split_rec_to_right(cursor, &split_rec)) {
direction = FSP_UP;
hint_page_no = page_no + 1;
} else if (btr_page_get_split_rec_to_left(cursor, &split_rec)) {
direction = FSP_DOWN;
hint_page_no = page_no - 1;
} else {
direction = FSP_UP;
hint_page_no = page_no + 1;
if (page_get_n_recs(page) > 1) {
split_rec = page_get_middle_rec(page);
} else if (btr_page_tuple_smaller(cursor, tuple, offsets, n_uniq, heap)) {
split_rec = page_rec_get_next(page_get_infimum_rec(page));
} else {
split_rec = NULL;
}
}
这里情况比较多,我们一一探讨:
btr_page_get_split_rec_to_right
// 这里返回TRUE表示将行记录向右分裂:即分配的新page的hint_page_no为原page+1// 且将page内较大的行记录移动至该新pageibool btr_page_get_split_rec_to_right(btr_cur_t *cursor, rec_t **split_rec)
{
page_t *page = btr_cur_get_page(cursor);
rec_t *insert_point = btr_cur_get_rec(cursor);
// 如果insert_point位于PAGE_LAST_INSERT处(对应顺序插入模式),LAST_INSERT代表上次插入位置 // insert_point代表小于等于待插入目标记录的最大记录位置 // 如果insert_point=PAGE_LAST_INSERT意味着本次待插入的记录是紧接着上次已插入的记录,也就是 // 这是一种顺序插入模式 // 1. 如果insert_point的下一条记录是supremum rec,即PAGE_LAST_INSERT是本页面最后一个用户记录 // 返回split_rec为nullptr。对于调用者,此时不会将任何记录移动至新page,而只在新page // 中插入目标record // 2. 如果如果insert_point的下下一条记录是supremum rec,会将下图case-2中的rec-next // 移动至新page,并将目标record插入至新page中 // 3. 如果上述1/2都不成立,意味着insert_point后至少有2条用户记录,从insert point的下下一条记录处开 // 始分裂,即在原page中保留PAGE_LAST_INSERT及其后面的一条记录(case-3中的rec-next),余下的记录 // 转移至新page,目标记录也插入原page // case-1: // ------------------------------------- // | ... | PAGE_LAST_INSERT | supremum | // ------------------------------------- // ^ // | // insert point // case-2: // ------------------------------------------------ // | ... | PAGE_LAST_INSERT | rec-next | supremum | // ------------------------------------------------ // ^ // | // insert point // case-3: // ---------------------------------------------------------------------- // | ... | PAGE_LAST_INSERT | rec-next | rec-next-next | ... | supremum | // ---------------------------------------------------------------------- // ^ | // | |-------> split rec // insert point // if (page_header_get_ptr(page, PAGE_LAST_INSERT) == insert_point) {
rec_t *next_rec = page_rec_get_next(insert_point);
if (page_rec_is_supremum(next_rec)) {
split_at_new:
*split_rec = NULL;
} else {
rec_t *next_next_rec = page_rec_get_next(next_rec);
if (page_rec_is_supremum(next_next_rec)) {
goto split_at_new;
}
*split_rec = next_next_rec;
}
return (TRUE);
}
return (FALSE);
}
上面的设计看起来比较复杂,但其背后的思想也比较好理解:
如果判定是顺序插入模式,便不会从中间开始分裂,而是从insert_point处开始分裂。这样可尽量保证左页面是满的而右页面很空,接下来的顺序插入肯定是插入新页面,相比于左页面和右页面平均分裂,这样可减少接下来的页面分裂次数,同时也不至于左页面内空间利用率不高。
btr_page_get_split_rec_to_left
ibool btr_page_get_split_rec_to_left(btr_cur_t *cursor, rec_t **split_rec)
{
page_t *page = btr_cur_get_page(cursor);
rec_t *insert_point = btr_cur_get_rec(cursor);
if (page_header_get_ptr(page, PAGE_LAST_INSERT) ==
page_rec_get_next(insert_point)) {
rec_t *infimum = page_get_infimum_rec(page);
// 假如插入的记录为20 // case-1: // -------------------------------------- // | infimum | rec-30 | rec-40 | rec-50 | // -------------------------------------- // ^ ^ // | |---> LAST_INSERT // insert point // 此时insert_point为infimum,其next为LAST_INSERT,判定为逆序插入 // 此时无需分裂现有page的任何行记录,只要将记录20插入新页面即可 // // case-2: // -------------------------------------------------------- // | infimum | rec-10 | rec-15 | rec-30 | rec-40 | rec-50 | // -------------------------------------------------------- // ^ ^ // | | // insert point LAST_INSERT // 此时要将20插入rec-15和rec-30之间,于是将rec-10分裂至左页面,腾出的一个rec空间插入20 // 由于是逆序插入,那么保证左页面比较空更容易减少后续的页面分裂概率 // case-3: // -------------------------------------------------------- // | infimum | rec-15 | rec-30 | rec-40 | rec-50 | // -------------------------------------------------------- // ^ ^ // | | // insert point LAST_INSERT // 此时要将20插入rec-15和rec-30之间,于是将rec-15分裂至左页面,腾出的一个rec空间插入20 // 由于是逆序插入,那么保证左页面比较空更容易减少后续的页面分裂概率 if (infimum != insert_point && page_rec_get_next(infimum) != insert_point) {
*split_rec = insert_point;
} else {
*split_rec = page_rec_get_next(insert_point);
}
return (TRUE);
}
return (FALSE);
}
其背后的设计思想同上面一样,只是这次模式是逆序插入,我们尽量保证左页面为空,有利于降低接下来页面分裂的概率。