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/ 最好看原版的,中文版的貌似頁數和課程中的對不上。

言歸正傳,本lab將實現一個Buffer Pool Manager,又分為三個子任務:

  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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM