通過幾天鏖戰終於完成了lab2,本lab實現一個支持併發操做的B+樹。簡直B格滿滿。node
B+樹本質上是一個索引數據結構。好比咱們要用某個給定的ID去檢索某個student記錄,若是沒有索引的話,咱們可能從第一條記錄開始遍歷每個student記錄,直到找到某個ID和咱們給定的ID一致的記錄。可想而知,這是很是耗時的。
若是咱們已經維護了一個以ID爲KEY的索引結構,咱們能夠向索引查詢這個ID對應的記錄所在的位置,而後直接從這個位置讀取這個記錄。從索引查詢某個ID對應的位置,這個操做須要高效,B+樹能保證以O(log n)的時間複雜度完成。git
B+樹由葉子節點和內部節點組成,和其它樹結構差很少,可是對(KEY, VALUE)的個數和排列順序有要求。github
格式以下:算法
* --------------------------------------------------------------------------- * | HEADER | KEY(1) + RID(1) | KEY(2) + RID(2) | ... | KEY(n) + RID(n) * ---------------------------------------------------------------------------
假設葉子結點最多能容納個n個(KEY, RID)對,那麼該葉子節點任什麼時候候都不能少於n/2向上取整個(KEY, RID)對。假設(KEY, RID)對個數爲x,那麼x必須知足:安全
ceil(n/2) <= x <= n
ceil表示向上取整,博客園不支持LaTeX o(╯□╰)o。
KEY是search key,RID是該KEY對應的記錄的位置。(KEY, RID)對按照KEY的増序進行排列。
HEADER的結構以下:數據結構
* ---------------------------------------------------------------------------------------- * | PageType (4) | LSN (4) | CurrentSize (4) | MaxSize (4) | ParentPageId (4) | PageId(4) | * ---------------------------------------------------------------------------------------
ParentPageId指向父節點。併發
* ---------------------------------------------------------------------------------------- * | HEADER | INVALID_KEY+PAGE_ID(1) | KEY(2)+PAGE_ID(2) | ... | KEY(n)+PAGE_ID(n) | * ----------------------------------------------------------------------------------------
假設內部節點最多容納n個(KEY, PAGE_ID)對,和葉子節點同樣,x必須知足:函數
ceil(n/2) <= x <= n
KEY表示search key,PAGE_ID指的是子節點的ID。
(KEY, PAGE_ID)對按照KEY的増序進行排列。
第一個KEY是無效的。
假設PAGE_ID(i)對應的子樹中的KEY用SUB_KEY表示,那麼SUBKEY都知足:KEY(i) <= SUB_KEY < KEY(i+1)。
測試
課本p489給出了find的僞代碼。總結來講就是先找到KEY應該出現的葉子節點,而後在該葉子節點中,查找KEY對應的RID。
以下圖:
假如咱們但願查找的KEY爲38,第一步在根節點A查找38應該出如今哪一個子節點中,根據以前的性質,38應該出如今以B爲根的子樹中,繼續查找節點B,以此類推,最終38應該出如今H的葉子節點中。最後咱們在H中查找38。
因此對於內部節點,咱們須要一個Lookup(const KeyType &key,const KeyComparator &comparator)方法,查找key應該出如今哪一個子節點對應的子樹中。線程
INDEX_TEMPLATE_ARGUMENTS ValueType B_PLUS_TREE_INTERNAL_PAGE_TYPE::Lookup(const KeyType &key, const KeyComparator &comparator) const { assert(GetSize() >= 2); // 先找到第一個array[index].first大於等於key的index(從index 1開始) int left = 1; int right = GetSize() - 1; int mid; int compareResult; int targetIndex; while (left <= right) { mid = left + (right - left) / 2; compareResult = comparator(array[mid].first, key); if (compareResult == 0) { left = mid; break; } else if (compareResult < 0) { left = mid + 1; } else { right = mid - 1; } } targetIndex = left; // key比array中全部key都要大 if (targetIndex >= GetSize()) { return array[GetSize() - 1].second; } if (comparator(array[targetIndex].first, key) == 0) { return array[targetIndex].second; } else { return array[targetIndex - 1].second; } }
由於KEY是已排序的,因此能夠先二分查找第一個大於或等於KEY的下標targetIndex,若是targetIndex對應的KEY就是咱們要找的KEY,那麼targetIndex對應的value就是下一步要搜索的節點,不然targetIndex-1對應的value是下一步應該搜索的節點。
課本p494給出了完整的insert(key, value)操做的僞代碼。
思路就是:
課本p498給出了完整的delete(key)操做的僞代碼。
思路:
最粗暴的方式就是在find, insert, delete開始就加鎖,執行完畢後解鎖,這樣邏輯上沒有問題,可是併發效率很低,至關於串行執行。
該協議容許多個線程同時訪問修改B+樹。
舉個查找過程的例子,查找key=38:
舉個插入過程的例子,插入25:
crab有螃蟹的意思,瞭解完crabbing協議加鎖的過程,應該不難理解爲何叫crabbing協議了吧。
咱們須要保護根節點id。
考慮下面這種狀況:
兩個線程同時執行插入操做,插入前B+樹只有一個節點,線程一插入當前key後將分裂,生成一個新的根節點。另外一個線程在線程一分裂前讀取了舊的根節點,從而將key插入到了錯誤的葉子節點中。
解決辦法:
在訪問,修改root_page_id_的地方加鎖,訪問或者修改完畢root_page_id_後釋放鎖。root_page_id_指向的是該B+樹的根節點,會保存在內存中,以便快速查找。