CMU15-445 Project.1总结

在线测试
CMU15-445 Project.1总结_第1张图片
本地测试
CMU15-445 Project.1总结_第2张图片

Project #1 - Buffer Pool

以下是Project #1的网址,2022FALL的Project #1是实现一个缓冲池。其中,为了实现缓冲池的管理,我们实现了缓冲池中的两个组件:可扩展哈希表和LRU-K 更换策略。最终利用可扩展哈希表来存储page与frame的对应关系,利用LRU-K 更换策略来实现frame的更新替换。而Buffer Pool Manager向系统提供了获得page的接口,只需要系统提供page_id就能够获得对应的page,而不需要关心如何获得。

1、可扩展哈希表

考虑到题目要求不能使用 built-in 的哈希表,我们需要自己实现可扩展哈希表用于维护Buffer Pool Manager中page与frame的对应关系。其中,Extendible Hash Table 由一个 directory 数组和多个 bucket 组成,其中 directory 数组的每一项都指向一个bucket :
directory:存放指向 bucket 的指针,是一个数组。用于寻找 key 对应 value 所在的 bucket。
bucket:存放 key 和 value,是一个链表。一个 bucket 可以至多存放指定数量的 key / value 对。

值得注意的是,在可扩展哈希表中,directory 数组的每一项都指向一个bucket,但一个bucket可以被多个directory 数组的项所指。 这主要与可扩展哈希表的插入机制有关:

  1. 当我们向哈希表中插入键值对 (K,V) 时,我们首先利用哈希函数计算出键值的哈希值std::hash()(key)。而后我们利用哈希值计算出当前键值对应当储存的对应bucket,并将其放入相应位置。
  2. 在计算哈希值查找对应的bucket时,我们遵循如下的规则:我们提出全局深度 global depth ,其初始值为0,我们利用std::hash()(key) & (1 << global_depth_) - 1,表示我们只使用哈希值的末 global depth 来确定对应的bucket位置。
  3. 但上述方法仍存在一定缺陷,当我们继续向当前的可扩展哈希表中插入元素,直至一个bucket已满无法继续插入新元素时,这是我们需要利用可扩展哈希表的机制,对当前的哈希表进行扩展。为了能够查找更多的bucket,我们需要增加 global depth 从而扩大识别bucket的范围。因此我们首先global depth++,这样做使得识别范围增大也会导致directory 容量翻倍。在此基础上,我们创建一个新的 bucket,并且需要重新安排原来 directory 对应 bucket 的指针。考虑到此时directory 的个数多于bucket的个数,因此往往此时会有多个directory 指向同一个bucket。为了实现上述的效果,我们针对每一个bucket 项单独设置一个local depth,当std::hash()(key) & (1 << local depth ) - 1,我们让相应的directory 指向对应的bucket 。而后我们重新分配 KV 对,将已满的bucket 中的每一项通过local depth计算后放到相应的位置中。在这样实现了bucket的分裂之后,我们再向bucket插入新的键值对。

值得注意的是,在可扩展哈希表中,我们的global depth 始终大于等于 local depth。因此我们在实际实现代码中,我们主要需要分成三种情况进行讨论:1、当当前bucket不满时,我们直接插入键值对即可;2、当当前bucket已满时,若当前bucket的local depth等于global depth时,说明我们需要对directory 进行扩容,我们需要将directory 增长为原先的两倍;3、当当前bucket已满时,若当前bucket的local depth小于global depth时,说明我们不需要增长directory ,我们只需要增加bucket并重新分配指针和键值对即可。

总结:

  1. 考虑到多线程的使用,而且会有多个直线指向同一个bucket,我们使用make_shared指针;
  2. 使用resize函数会同时增加容器的size和capacity,而使用reserve只增加capacity,需要继续添加元素以免出现内存泄漏;
  3. 在判断已满的bucket中键值对应如何分配时,显然此时他们的末local depth都是相等的,我们可以直接根据末local depth+1位进行判断即可;
  4. 考虑到多线程进行操作,我们使用智能锁std::scoped_lock lock(latch_);进行处理。

2、LRU-K 更换策略

LRU-K Replacer 用于存储 buffer pool 中 page 被引用记录,并根据记录选出buffer pool 满时需要被驱逐的 page。其中不同于传统的LRU策略,我们会对引用此时大于等于K次的page进行单独处理。当需要进行驱逐时,我们优先在引用次数小于K次的page中找出引用时间最早的进行驱逐;当所有page引用次数都大于等于K次时,我们在这些page中找到被引用时间最早的进行驱逐。

在进行代码实现时,我们添加了一个类FrameInfo用于记录当前Frame的信息,包括了其使用次数、可驱逐标志、ID等。而后我们设置了两个std::list>列表分别用于记录使用次数大于K次的page和使用次数小于K次的page;并利用两个哈希表std::unordered_map>::iterator>来记录ID与FrameInfo之间的对应关系,同样按引用次数进行划分成两类。

  1. Evict函数中,我们首先在使用次数小于K次的page列表temp_pool_中进行查找,判断是否有page可驱逐,而后在使用次数大于等于K次的page列表cache_pool_中进行查找。在找到可驱逐项之后,需要同时删除列表和哈希表中的对应项;
  2. RecordAccess函数中,我们首先利用断言判断非法情况。而后我们同样在cache_pool_和temp_pool_中查找是否存在对应项,若存在我们先将其删除后,而后将其插入列表末尾,并更新哈希表中的指针,并增加相应项的使用次数;
  3. SetEvictable函数中,我们首先在temp_pool_和cache_pool_中查找对应项,若存在则更新其相应的可驱逐标志;
  4. Remove函数中,我们同样在temp_pool_和cache_pool_中查找对应项,同时需要判断相应项的可驱逐标志是否为true,否则需要抛出异常。在查找到之后需要在列表和哈希表中删除对应项;
  5. Size函数中,我们需要遍历整个temp_pool_和cache_pool_并统计所有可被驱逐的对象。

总结:

  1. 利用哈希表和列表来记录每一项,避免了对于时间戳和个数的讨论;
  2. 通过设计FrameInfo类,能够方便获取对应项的所有信息;
  3. 利用断言抛出异常。

3、缓冲池管理器实例

在实现了上述两个组件之后,我们可以在Buffer Pool Manager中直接调用我们设计好的函数,其中主要包括了几个对象:

  1. pages:存储 page 的指针数组;
  2. disk_manager_:向磁盘中读写文件;
  3. page_table_:任务一中的可扩展哈希表,记录page_id和frame_id的对应关系,即 page 在 buffer pool 中的位置;
  4. replacer_:任务二中的LRU-K 更换策略,用于更新page ;
  5. free_list_:记录当前缓冲池中为空的frame。

在进行代码实现时,我们采用如下的思路:

  1. NewPgImp函数中,我们首先判断当前缓冲池中是否有空闲的frame,若是则直接获取该frame;若不存在空闲frame,我们首先利用replacer_判断是否存在可驱逐的page :若不存在则直接返回nullptr;若存在我们根据IsDirty判断是否需要向磁盘中写文件,而后我们在page_table_中删除当前的page 与frame对应关系。而后我们利用AllocatePage函数新建page ,并重置更新原先frame的信息page_id_、pin_count_、is_dirty_,向page_table_中更新项,并调用RecordAccessSetEvictable表示当前frame被使用;
  2. FetchPgImp函数中,我们首先在page_table_查找是否能找对对应的page_id:若能则调用RecordAccessSetEvictable表示当前frame被使用,并更新其使用的进程数;否则我们进行讨论:1、若当前有空闲的frame,我们直接获取该frame;2、若不存在空闲frame,我们首先利用replacer_判断是否存在可驱逐的page :若不存在则直接返回nullptr;若存在我们根据IsDirty判断是否需要向磁盘中写文件,而后我们在page_table_中删除当前的page 与frame对应关系。而后我们更新当前frame中的信息page_id_、pin_count_、is_dirty_,向page_table_中更新项,并调用RecordAccessSetEvictable表示当前frame被使用;
  3. UnpinPgImp函数中,我们首先判断能否在page_table_中找到对应page_id,并判断其是否还有进程使用,否则返回false。而后我们减少进程数,并根据新的进程数判断是否需要修改可驱逐标志以及当前page是否为脏;
  4. FlushPgImp函数中,我们首先判断当前page_id是否有问题,而后我们根据page_id找到对应frame_id进行向磁盘中写文件,并更新is_dirty_;
  5. FlushAllPgsImp函数中,我们遍历当前pages_,首先判断当前page_id是否有问题,而后直接向磁盘中写文件并更新is_dirty_;
  6. DeletePgImp函数中,我们首先在page_table_中查找相应的page_id,并判断其对应的进程数。而后我们从page_table_和pages_中释放相应项,并利用replacer_删除相应的frame_id,最终将frame加入空闲列表中,并释放frame空间。

总结:

  1. 更新frame信息时需要注意与page的对应关系、进程数、脏标志、可驱逐标志;
  2. 根据脏标志判断是否需要写文件;
  3. 存在INVALID_PAGE_ID,需要针对写一个返回。

你可能感兴趣的:(CMU,15-445,哈希算法,链表,数据结构)