天天看點

[MySQL 源碼] Innodb Pessimistic Insert流程

簡單跟了下插入導緻索引分裂的流程

//////////////////////////////////

入口函數: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);