CMU-15445 LAB1:Extendible Hash Table, LRU, BUFFER POOL MANAGER

概述

最近又開了一個新坑,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

  1. 實現一個Extendible Hash Table
  2. 實現一個LRU Page Replacement Policy
  3. 實現Buffer Pool Manager

Extendible Hash Table

Extendible Hash Table是動態hash的一種,動態是相對靜態來講的。hash的原理是經過hash函數,f(key)->B,將key映射到一個Bucket地址集合中,若是B集合選的比較小,那麼當key增多後,愈來愈多的key會落在同一個Bucket中,這樣查找效率會降低。若是B集合一開始就選的很大,那麼有不少Bucket處於未滿狀態,浪費空間。爲了解決這個問題,就引入動態hash的概念。
靜態hash存在上述問題主要是hash函數肯定好後就不能再變了。動態hash就沒有這個問題。算法

數據結構

Extendible Hash Table數據結構以下:
1_lab1_extandable_hashing_data_structure數據庫

  1. bucket address table是一個數組,保存bucket的地址。
  2. global depth是一個整數值。
  3. 每一個bucket都有一個local depth也是一個整數值,且小於等於global depth。每一個bucket能裝的鍵值對的最大值爲bucketMaxSize。

查詢

好比要查找key=1對應的value值,首先取h(1)對應的二進制前global depth位,做爲bucket address table的下標,找到存放該key的bucket,而後在相應的bucket中查找。數組

插入

2_lab1_extandable_hashing_1

如上圖,假設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。指針

LRU PAGE REPLACEMENT POLICY

實現最近最少使用算法,說白了就是給你一些序列,好比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是否已經在隊列中。
最終肯定數據結構以下:
3_lab1_LRU_data_structure

BUFFER POOL MANAGER

爲何須要BUFFER POOL MANAGER

假設兩種極端的狀況:

  1. 沒有緩衝池,那麼數據都位於磁盤上,第一次訪問一頁數據,須要將其從磁盤讀取到內存,第二次在訪問相同的頁時,還須要從磁盤讀,很是耗時。
  2. 假設內存無限大,那麼訪問一頁數據後,將該頁數據直接保存到內存,下次再訪問該頁時,直接訪問內存緩存就行。可是現實中內存比磁盤容量小得多,只能緩存有限個數據頁,以下圖內存只能緩存三個頁,依次訪問PAGE 1, 2, 3, 如今已經緩存了PAGE 1, 2, 3,假設想讀取PAGE 4,那麼得先清空一個內存緩存頁,用來緩存PAGE 4的數據,那麼清除誰呢?。這時候任務2的替換策略就派上用場了,根據LRU替換策略,PAGE 1是最近最久沒有被使用過的,那麼就將PAGE 1從新寫回到磁盤,而後將PAGE 4讀取到內存。
    4_lab1_buffer_pool

因此BUFFER POOL MANAGER的做用是加速數據的訪問,同時對使用者來講是透明的。

具體代碼就不貼了,能夠參考個人實現:https://github.com/gatsbyd/cmu_15445_2018

相關文章
相關標籤/搜索