Overview
Project 1 主要和Bustub的存储管理相关,分为三个部分
- Extendible Hash Table(可扩展哈希表)
- LRU-K Replacer
- Buffer Pool Manager
其中可扩展哈希表和LRU-K Replacer是Buffer Pool Manager内部的组件。
Buffer Pool Manager只是向外部提供了获取page的接口,系统拿着一个page_id就可以向Buffer Pool Manager索要对应的page,而不用关心这个page具体存放在哪,系统并不关心(也不可见)获取这个page的过程,无论是从磁盘上还是内存中读取,还是page可能发生的在磁盘和内存之间的移动。这些内部的操作都是交给Buffer Pool Manager来完成。
可扩展哈希表是用于从磁盘上获取的页号到实际存放在内存中的页号的映射。
LRU-K Replacer则用于记录页的访问历史记录以及在Buffer Pool 满时根据访问页的历史记录驱逐对应的页。
Task 1 Extendible Hash Table
这一部分实现的是可扩展哈希表,其在Buffer Pool Manager中主要用来存Buffer pool中page_id和frame_id的映射关系。
Extendible Hash Table由一个directory(目录)和多个bucket(桶)组成。
- directory:存放指向bucket的指针,是一个数组。用于寻找key对应的value所在的kucket。
- bucket:存放value,是一个链表,一个bucket可存放的value数在初始化时指定。
可扩展哈希表和一般的链哈希表(用链地址法解决冲突)最大的区别是,可扩展哈希中,不同的指针可以指向同一个bucket,而链哈希表中每个指针对应一个bucket。
发生冲突的时候,链哈希表将新的value追加到其key对应bucket链表的最后面,也就是说链哈希表的bucket没有容量上限。而可扩展哈希中,如果bucket达到容量上限,则会对桶进行一次split操作(桶分裂)。
- 可扩展哈希表的插入流程
将一个键值对(K,V)插入哈希表时,会先用哈希函数计算K的哈希值H(K),并用此哈希值计算出索引,将V放入索引对应的bucket中。
可扩展哈希计算索引的方式是直接取哈希值H(K)的低n位。这里的n叫做global depth。
例如,如果K对应的哈希值H(K)为1010 0010b,此时global depth为4的话,那index就是0010,即应该将V放入directory里index为2的指针指向的bucket里。
与global depth对应,每一个bucket都有对应的loacl depth,local depth 和 global depth的初始值都是0。对于一个(K,V)对,bukcet只用到了K的哈希值H(K)的低local depth位作为索引。
具体实现:
如果插入键值对时,K是已经存在的,那只需要修改K所在的bucket里面的(K,V);
如果插入键值对时,K不存在且bucket还未满,那可以直接将(K,V)插入bucket;
如果插入键值对时,K不存在且bucket已满,那就需要判断一下当前bucket的local depth是否等于global depth,如果local depth小于global depth,那就说明在directory是有多个指针指向当前bucket的,此时需要完成桶分裂,新建一个bucket之后(此时新建的bucket与原先的bucket,local depth都比原来大1),重新分配directory原先指向当前bucket的多个指针,更新这些指针现在应该指向的位置。
因为bucket的local depth增大之后,bucket对(K,V)对的区分度更大,可以根据增大的local depth对应的那一位是0还是1将directory原先指向当前bucket的多个指针指向当前的bucket或者新建的bucket,之后再把分裂的bucket里的(K,V)对重新分配即可。
最后,如果global depth扩展过了,那么需要修改一下directory新增的指针,如果其指向为nullptr,则将其指针的指向置为当前index % ((globaldepth - 1)的2次方)指向的bucket。
因为要支持并发操作,可扩展哈希表需要保证线程安全,现行策略是对可扩展哈希表进行查找、插入和删除时都要先使用锁,保证不同线程互斥访问。
- 可扩展哈希表的优点
可扩展哈希表(Extendible Hash Table)相比一般的哈希表在某些方面具有一些优点,特别是在动态数据集和并发操作方面。可扩展哈希表相对于一般哈希表的一些优点如下:
-
动态调整大小:可扩展哈希表可以动态地调整自身的大小,以适应数据集的变化。这意味着在数据集增大时,可扩展哈希表可以自动增加桶的数量,从而保持较低的冲突率和较好的性能。相比之下,传统的哈希表可能需要重新哈希或者复制整个数据集来调整大小。
-
较低的冲突率:可扩展哈希表的动态调整大小使得冲突率相对较低。由于数据在桶之间分布得更均匀,每个桶的负载较小,这可以提高查找、插入和删除操作的效率。
-
适用于动态数据集:对于数据集大小经常变化的情况,可扩展哈希表可以更好地应对。它可以避免频繁的冲突和性能下降,而一般的哈希表可能需要定期重新哈希以维持性能。
-
并发性能:可扩展哈希表的动态调整大小使得在并发环境下具有更好的性能。多个线程可以同时操作不同的桶,而无需等待全局锁。这可以提高并发访问的效率。
-
拓展性:可扩展哈希表对于处理非均匀分布的数据集也更具拓展性。当某些桶的负载较大时,可扩展哈希表可以更灵活地调整以适应这种情况,而传统的哈希表可能需要重新设计哈希函数或进行其他调整。
尽管可扩展哈希表在上述方面具有优势,但也需要注意它可能会引入一些额外的开销,例如维护目录结构和桶的指针,以及拆分和合并桶时的复杂操作。在具体的应用场景中,需要权衡可扩展哈希表的优势和开销,以确定是否适合使用。
Task 2 LRU-K Replacer
LRU-K Replacer用于存储buffer pool中page的访问记录,并根据访问记录来选出在buffer pool满时需要被驱逐的page。
LRU-K 是 LRU的一个变种
如果使用LRU,我们仅需要记录page最近一次被访问的时间,在驱逐时,选择最近一次引用时间最早的page。
在LRU-K中,我们需要记录page最近K次被访问的时间,假如在list中所有page都被访问了大于等于K次,则比较最近第K次被访问的时间,驱逐访问时间最早的,假如list中存在访问次数少于K次的page,则从这些page中挑选出第一次被访问时间最早的进行驱逐。
具体实现:
定义两个自定义排序规则的Set(根据最早访问时间对Set里面的页进行排序,访问最早的页排在前面),这两个Set里面存储的均是可被驱逐的页。
第一个Set存放的是小于K次访问的页,
第二个Set存放的是大于等于K次访问的页,
访问页时,记录访问时间并将页访问次数加1,如果页是可驱逐的(什么情况下不可驱逐?当上层调用者需要进行一些读写操作,此时需要保证page可以暂时驻留在内存里面),根据页的访问次数将页加入第一个Set或者第二个Set。
驱逐时,如果上述两个Set是空的,则驱逐失败,否则:
如果第一个Set是非空的,取第一个Set的第一个页驱逐,如果第一个Set是空的,那就取第二个Set的第一个页进行驱逐。
使用LRU-K驱逐策略相比LRU有什么好处?
可以减少抖动,避免频繁地将一个页面从buffer pool中移除,然后又重新加载,导致频繁的磁盘访问。由于LRU-K算法考虑了过去K次访问,如果一个页面被访问过K次,那这个页面被驱逐的可能性相比原先使用LRU算法进行驱逐要低,方便后续还可能对此页面进行访问。
Task 3 Buffer Pool Manager Instance
基于上述Task 1和Task 2实现的可扩展哈希表来完成磁盘页面到内存页面页号的映射,LRU-K replacer用来记录页面的访问历史以及buffer pool满时驱逐页面。
这个任务主要要完成以下几个接口:
FetchPgImp(page_id)
UnpinPgImp(page_id, is_dirty)
FlushPgImp(page_id)
NewPgImp(page_id)
DeletePgImp(page_id)
FlushAllPagesImpl()
FetchPgImp用于读取磁盘中page_id指定的页,如果该页面已经读到buffer pool里面,那修改相关的pin_count等属性即可,注意此时要设置为不可驱逐,在上层调用访问完该页之后再将该页的pincount-1,当pincount为0时,才可驱逐页面。如果页面不在buffer_pool里面,那就从磁盘加载该页面到buffer pool的一个空闲页中,如果没有空闲页了,那使用LRU-K replacer驱逐一个页面,再加载指定磁盘页到buffer pool中。
UnpinPgImg用于减少页面的pincount(-1),如果页面不存在于buffer pool中,直接返回false,如果pincount已经为0,返回false,否则将pincount-1,如果pincount为0,要将该页设置为可驱逐。同时该接口也可以设置一个页是否是脏页,即是否被修改过。
FlushPgImp用于刷新一个指定的页到磁盘
NewPgImp用于加载一个新页到buffer pool中,如果buffer pool满了,则驱逐一个页
DeletePgImp用于将一个页从buffer pool中移除,移除时要删除相应的数据结构里面的记录,如果该页是脏页,那在将其从内存中删除前需要将该页写回磁盘。
FlushAllPagesImpl用于刷新当前buffer pool里面的所有页到磁盘中。