最近又開了一個新坑,CMU的15445,這是一門介紹數據庫的課程。我follow的是2018年的課程,由於2018年官方中止了對外開放實驗源碼,因此我用的2017年的實驗,可是問題不大,內容基本沒有變化。想要獲取實驗源碼的同窗能夠上github搜,或者直接clone個人代碼,找到最先的commit就ok了,倉庫地址在文末。課程配套教材是《
Database System Concepts》,https://book.douban.com/subject/4740662/ 最好看原版的,中文版的貌似頁數和課程中的對不上。git
言歸正傳,本lab將實現一個Buffer Pool Manager,又分爲三個子任務:github
Extendible Hash Table是動態hash的一種,動態是相對靜態來講的。hash的原理是經過hash函數,f(key)->B,將key映射到一個Bucket地址集合中,若是B集合選的比較小,那麼當key增多後,愈來愈多的key會落在同一個Bucket中,這樣查找效率會降低。若是B集合一開始就選的很大,那麼有不少Bucket處於未滿狀態,浪費空間。爲了解決這個問題,就引入動態hash的概念。
靜態hash存在上述問題主要是hash函數肯定好後就不能再變了。動態hash就沒有這個問題。算法
Extendible Hash Table數據結構以下:
數據庫
好比要查找key=1對應的value值,首先取h(1)對應的二進制前global depth位,做爲bucket address table的下標,找到存放該key的bucket,而後在相應的bucket中查找。數組
如上圖,假設bucketMaxSize爲2.緩存
最開始的狀況如figure1,咱們插入[1, v], [2, v],由於這時global depth=0因此,所有落在bucket1中,也就是figure2。數據結構
在figure2基礎上,再插入[3, v],這時仍是應該插到bucket1中,可是bucket1已經滿了,同時bucket1的local depth = global depth = 1。這時先將bucket address table擴大一倍,同時global depth加1。而後從新建立兩個新的bucket a, bucket b,local depth在原來local depth基礎上加1(由0變爲1),再將bucket 1中的[1, v], [2, v]分配到新的兩個bucket中,分配規則以下:
若是h(key)的第local depth(1)位是0,那麼放到bucket a中,若是爲1那麼放到bucket b中。分配完畢後,從新調整bucket address table中指向原來bucket 1的指針指向,這裏index 0和1的指針原來都指向bucket 1,因此都須要調整,調整規則以下:
index的第local depth(1)位爲0的指向bucket a, 爲1的指向bucket b。
最後在插入[3, v], 假設h(3)的前global depth爲1,那麼插入到bucket b中。最終的效果如figure3。函數
在figure3基礎上再插入[4, v],算法和前面同樣,假設[4, v]本應插入到bucket a中,可是bucket a滿了,且global depth = bucket 1的local depth。因此先將bucket address table擴大一倍。而後從新建立兩個新的bucket, bucket c和bucket d,再將bucket a中的[1, v], [2, v]從新分配到bucket c和bucket d中。在調整buckert address table指針指向,最後再插入[4, v]。最終效果如figure 4。性能
在figure4基礎上,再插入[5, v], [6, v],假設都落在bucket b中,那麼插入[5, v]後bucket b將滿,再插入[6, v]的時候bucket b已經滿了。這時和前面不同,此時global depth(2) > bucket b的local depth(1)。因此不須要擴大bucket address table。只須要建立兩個新的bucket, bucket e和bucket f。將原來bucket b中的[3, v], [5, v]分配到bucket e和bucket f中。而後調整原來指向bucket b的指針指向bucket e和bucket f。最後在插入[6, v]。最終效果如figure 5。指針
實現最近最少使用算法,說白了就是給你一些序列,好比1, 2, 3, 1,這時哪一個是最近最少使用到的。能夠畫下圖,越下面的越久沒有使用到。先用了1,再用了2,那麼2比1新,因此2在1上面,而後用了3,那麼3應該在2的上面,最後用了1,那麼把1從最下面調到最上面,同時2變到了最下面,至此2應該是最近最久沒有使用的。
1 2 3 1 1 2 3 1 2
那麼用什麼數據結構來存儲呢?
先看下有哪些操做:
void Insert(const T &value); bool Victim(T &value); bool Erase(const T &value);
Insert():將value加到最頂部,或者若是value已經在隊列中,將其提取到最頂部。
Victim():提取最近最久沒有使用的元素,將最底部的元素彈出。
Erase():刪除某個元素。
首先想到的是單向鏈表。可是若是用單向鏈表的話,Victim()須要訪問尾元素,單向鏈表每次都要從頭至尾遍歷一遍才能訪問尾元素,性能可想而知。
用雙向鏈表就能夠解決這個問題,雙向鏈表能夠以O(1)的時間訪問頭尾元素。還有個問題,若是調用Insert(v),按照以前的算法,我先得知道v在不在這個雙向鏈表中,若是不在直接插到頭部,若是在的話,將其提取到頭部。若是僅僅是雙向鏈表,那麼仍是須要遍歷一遍隊列,查詢v是否是已經在隊列中了。
能夠用一個map記錄已經在隊列中的元素到鏈表節點的鍵值對,這樣就能夠以O(1)的時間查詢某個value是否已經在隊列中。
最終肯定數據結構以下:
假設兩種極端的狀況:
因此BUFFER POOL MANAGER的做用是加速數據的訪問,同時對使用者來講是透明的。
具體代碼就不貼了,能夠參考個人實現:https://github.com/gatsbyd/cmu_15445_2018