簡單跟了下插入導緻索引分裂的流程
//////////////////////////////////
入口函數:row_ins_index_entry
實際上悲觀插入和樂觀插入是根據row_ins_index_entry_low的第一個參數來判斷的
調用兩次row_ins_index_entry_low
第一次參數為btr_modify_leaf,表示隻修改葉子節點,如果失敗了
第二次參數為btr_modify_tree,表示需要修改b-tree,這時候會選擇調用函數:
btr_cur_pessimistic_insert:
2.檢查鎖,如果是聚集索引,還要記錄undo,并記錄復原段指針,對于非聚集索引,要在page上記錄最大事務id
之前已經分析過,這裡不贅述。
err = btr_cur_ins_lock_and_undo(flags, cursor, entry,
thr, mtr, &dummy_inh);
3.擴充檔案,為ibd預留足夠的檔案空間
n_extents = cursor->tree_height / 16 + 3;
success = fsp_reserve_free_extents(&n_reserved, index->space,
n_extents, fsp_normal, mtr);
4.檢查目前記錄是否需要進行外部存儲(page_zip_rec_needs_ext),如果需要的話,則需要對記錄進行處理
在函數dtuple_convert_big_rec中,會去循環查找,能最大減少rec的外部存儲列
但固定長度列、空列、外部存儲列或則長度小于40位元組的列不做考慮
另外,在是用dynamic和compressed格式時,任何最大長度小于256位元組的非blob列都是本地存儲;而對于redundant和compact類型而言,最大不超過788位元組時都會本地存儲。
big_rec_vec = dtuple_convert_big_rec(index, entry, &n_ext);
傳回值類型為big_rec_struct,用于存儲溢出資料。
如果找不到滿足要求的最大列,傳回null。
如果找到了,則替換原記錄上的外部指針,并存儲實際資料。
如果依然不滿足頁記憶體儲,則繼續尋找該記錄上更多的列來進行外部存儲。
5.開始進行索引分裂:
a.如果目前記錄的cursor在根page上,則分裂節點,提升btree高度,然後再插入記錄
*rec = btr_root_raise_and_insert(cursor, entry, n_ext, mgr);
1)首先為b-tree配置設定一個新page,并進行初始化前後page指針 level = btr_page_get_level(root, mtr); new_block = btr_page_alloc(index, 0, fsp_no_dir, level, 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, mgr); btr_page_set_next(new_page, new_page_zip, fil_null, mtr); btr_page_set_prev(new_page, new_page_zip, fil_null, mtr); 2)将root節點的記錄一個個拷貝到new_page中 /* copy the page byte for byte. */ page_zip_copy_recs(new_page_zip, new_page, root_page_zip, root, index, mgr); //拷貝記錄 /* update the lock table and possible hash index. */ lock_move_rec_list_end(new_block, root_block, //page間遷移記錄鎖 page_get_infimum_rec(root)); btr_search_move_or_delete_hash_entries(new_block, root_block, index); //更新ahi 例外還需要将root頁上supremum記錄上的鎖遷移到新block的supremum記錄上,這在做悲觀更新時會發生鎖托管到系統記錄上 lock_update_root_raise(new_block, root_block); 3).建構node ptr 讀取新page上的第一個使用者記錄,建立節點指針(節點鍵值+page no) rec = page_rec_get_next(page_get_infimum_rec(new_page)); new_page_no = buf_block_get_page_no(new_block); node_ptr = dict_index_build_node_ptr(index, rec, new_page_no, heap, level); 設定node_ptr的flag為rec_info_min_rec_flag,表面這是該層的最小記錄 dtuple_set_info_bits(node_ptr, dtuple_get_info_bits(node_ptr) | rec_info_min_rec_flag); 4)清空重置root節點,并将node ptr寫入root節點 btr_page_empty(root_block, root_page_zip, index, level + 1, mgr); //清空root page btr_page_set_next(root, root_page_zip, fil_null, mgr); //設定page的前後page為null 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); node_ptr_rec = page_cur_tuple_insert(page_cursor, node_ptr, //将node ptr插入到root page中 index, 0, mtr); 5).重置記錄cursor page_cur_search(new_block, index, tuple, page_cur_le, page_cursor); 并對新page進行split btr_page_split_and_insert(cursor, tuple, n_ext, mtr) 也就是下面普通的page分裂流程
b.如果目前記錄cursor不在根節點page上,走一般的索引分裂邏輯
*rec = btr_page_split_and_insert(cursor, entry, n_ext, mtr);
該函數會先進行page分裂,然後插入記錄。
1)選擇作為分裂中間點的記錄; 先介紹涉及到的幾個宏 #define fsp_up ((byte)111) /*!< alphabetically upwards */ #define fsp_down ((byte)112) /*!< alphabetically downwards */ #define fsp_no_dir ((byte)113) /*!< no order */ 這幾個宏決定記錄插入新分裂頁的順序,是按照字母上升順序,還是下降順序。 分三種情況來決定分裂記錄和方向 <1>.已經做過一次split,但記錄依然無法插入成功,則繼續進行分裂 direction = fsp_up; hint_page_no = page_no + 1; split_rec = btr_page_get_split_rec(cursor, tuple, n_ext); //查找一個分裂記錄 if (univ_unlikely(split_rec == null)) { insert_left = btr_page_tuple_smaller( cursor, tuple, offsets, n_uniq, &heap); <2>.如果函數btr_page_get_split_rec_to_right傳回true,則: direction = fsp_up; int_page_no = page_no + 1; 函數btr_page_get_split_rec_to_righ流程如下: 首先判斷本次插入記錄是否在上次插入記錄的右邊,如果是的話,則認為這是順序的插入,然後執行如下: –>擷取目前插入記錄的下一個記錄,如果是supremum record,則split_rec=null, –>再次獲得下下條記錄,也就是說,會向後看兩條記錄,這兩條記錄有一條為supremum record,split_rec都會被設定為null, 否則設定split_rec為第二個記錄。 這樣做的目的是,如果從插入點開始有超過兩個使用者記錄,我們在該page上保留一個記錄,這樣随後的序列插入可以利用自适應哈希,因為他們可以簡單的通過檢視這個page上的記錄,來檢查正确的查找位置(翻譯自注釋,待證明),split_rec為null表示從新插入的記錄開始分裂. –>傳回true 如果是随機插入的話,傳回false <3>如果函數btr_page_get_split_rec_to_left傳回true,則 direction = fsp_down; hint_page_no = page_no – 1; ut_ad(split_rec); 這種情況下split_rec不可為空 函數btr_page_get_split_rec_to_left流程如下 首先判斷,如果上次插入的記錄在目前插入記錄的右邊,則繼續下面的流程,否則傳回false –>如果插入的記錄不是目前page上的第一個記錄,也就是不在infimum記錄的下一個,設定split_rec為目前插入點 –>否則,設定split_rec為目前記錄插入點的下一個 <4>如果上述都不滿足 direction = fsp_up; hint_page_no = page_no + 1; 如果page上記錄不止1個,則從中間開始分裂 split_rec = page_get_middle_rec(page); 如果目前插入記錄比該page上記錄要小(btr_page_tuple_smaller),則 split_rec = page_rec_get_next( page_get_infimum_rec(page)); 否則,split_rec為null 也就是說,當這個page上隻有一個記錄時,我們不能從中間做分裂,而是需要決定新節點是插入到左邊還是右邊節點。 綜上,隻有當上次插入的記錄在目前插入點的右邊時,才會設定direction = fsp_up,其他情況都是fsp_down 2)為索引配置設定一個新page new_block = btr_page_alloc(cursor->index, hint_page_no, direction, btr_page_get_level(page, mtr), mtr); btr_page_create(new_block, new_page_zip, cursor->index, btr_page_get_level(page, mtr), mtr); 3)計算上半部分的page的第一個記錄,以及在原始page上的第一個記錄 <1> 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, offsets) < 0; insert_left表示目前記錄是插入到split_rec的左邊還是右邊。 <2>insert_left為true //隻有在n_iterations>0時才會發生 first_rec = page_rec_get_next(page_get_infimum_rec(page)); move_limit = page_rec_get_next(btr_cur_get_rec(cursor)); <3>其他(split_rec為空) buf = mem_alloc(rec_get_converted_size(cursor->index, tuple, n_ext)); first_rec = rec_convert_dtuple_to_rec(buf, cursor->index, tuple, n_ext); first_rec為插入的記錄 move_limit為目前插入記錄的下一條 4)修改b樹結構 <1>将新page attach到btree上對應的層次上,并向上層節點插入node指針,更新目前層次的節點連結清單指針 btr_attach_half_pages(cursor->index, block, first_rec, new_block, direction, mtr); <2>判斷新記錄是否能夠滿足插入。 –>split_rec不為空: insert_will_fit = !new_page_zip && btr_page_insert_fits(cursor, split_rec, offsets, tuple, n_ext, heap); 對于壓縮表,new_page_zip不為空,也就是說insert_wil_fit總是為false。 –>split_rec為空,表示分裂記錄就是目前插入記錄 insert_will_fit = !new_page_zip && btr_page_insert_fits(cursor, null, null, tuple, n_ext, heap); 同樣的insert_will_fit對于壓縮表而言,總是為false。這意味着在索引分裂的過程中,會一直持有索引上的排他鎖 這也會導緻壓縮表的分裂開銷非常大。 <3>判斷是否釋放索引上的排他鎖 if (insert_will_fit && page_is_leaf(page)) { mtr_memo_release(mtr, dict_index_get_lock(cursor->index), mtr_memo_x_lock); } 5)開始做記錄遷移,根據direction的不同,走不同的分支,流程大同小異,這裡我們主要看一下fsp_up <1>看該函數的傳回值: page_move_rec_list_end(new_block, block, move_limit, cursor->index, mtr) 将block上從move_limit(包含該記錄)開始記錄拷貝到new_block中。 –>page_copy_rec_list_end(new_block, block, split_rec, index, mtr) 實際拷貝動作,在拷貝完成後,還會對新page做compress 如果傳回false,表示壓縮失敗,直接傳回false。不繼續下面的流程 –>從block上删除轉移的記錄。 page_delete_rec_list_end(split_rec, block, index, new_n_recs – old_n_recs, new_data_size – old_data_size, mtr); –>傳回true 如果轉移記錄到新block,但新block壓縮失敗的話,還需要繼續做處理: –>将原page中的記錄拷貝到新page中。 page_zip_copy_recs(new_page_zip, new_page, page_zip, page, cursor->index, mtr); –>從新page上删除從move_limit開始的記錄,不包括move_limit記錄以及系統記錄 page_delete_rec_list_start(move_limit – page + new_page, new_block, cursor->index, mtr); –>更新鎖表和ahi lock_move_rec_list_end(new_block, block, move_limit); btr_search_move_or_delete_hash_entries( new_block, block, cursor->index); –>從源表上删除從move_limit(包括move_limit)開始的記錄 page_delete_rec_list_end(move_limit, block, cursor->index, ulint_undefined, ulint_undefined, mtr); 最後更新記錄鎖 left_block = block; right_block = new_block; lock_update_split_right(right_block, left_block); 6)到了這一步,索引樹的分裂和修改已經結束了,這時候可以把記錄插入到其中,根據insert_left來選擇插入到哪個block中 7)如果插入失敗,則重新reorgnize page,n_iterations++;然後重頭開始繼續分裂。 8)對于二級索引,更新insert buffer.
6.更新adaptive hash index
btr_search_update_hash_on_insert(cursor);
7.更新記錄鎖資訊
lock_update_insert(btr_cur_get_block(cursor), *rec);
8.如果之前配置設定了extend,則更新space->n_reserved_extents計數
if (n_extents > 0) {
fil_space_release_free_extents(index->space, n_reserved);