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

相关文章
相关标签/搜索