2021 CMU-15445/645 Project #2 : Hash Index

0 前言

21年CMU-15445的Project2是实现一个基于可拓展哈希(EXTENDIBLE HASH)的哈希索引。比起Project 1,Project2的难度要高不少,我自己跑完所有的测试花了40s,只能算是一个中规中矩的成绩,在gradescope上好一点的成绩可以压缩到30s以内。不过我还是把它写成博客,希望能帮助到没有头绪的朋友,也希望完成的比较好的朋友能指正。
下面说一下Project2的要点,不过按照课程要求,这里就不放代码了。对于EXTENDIBLE HASH算法理解有困难的朋友可以在github看看这个实现,对完成Project极有帮助。

1 Hash Directory Page

HashTableDirectoryPage类起到一个目录作用,拿到一个Key,判断交给哪个bucket处理它的数据成员如下:

  page_id_t page_id_; //page在buffer pool manager的id
  lsn_t lsn_;
  uint32_t global_depth_{0}; //全局深度
  uint8_t local_depths_[DIRECTORY_ARRAY_SIZE];//每个bucket目前的深度
  page_id_t bucket_page_ids_[DIRECTORY_ARRAY_SIZE]; //每个bucket对应的page id

这个类的成员函数实现不难,唯一需要关注的是

uint32_t GetGlobalDepthMask() {
  return (1 << global_depth) - 1}

它的作用是返回一个掩码,掩码决定了一个Key/Value对会被放入哪个bucket里面。如key的hash值是10,depth为2,则放入1010 & 0011 = 2号桶里面。

2 Hash Bucket Page

HashTableBucketPage顾名思义就是一个桶,一个HashTable需要有很多个桶来存放数据。拿到一个Key,桶需要根据需要完成插入、删除、查询操作。这个类下面三个数据成员初次接触有点难理解,下面解释一下。


  // For more on BUCKET_ARRAY_SIZE see storage/page/hash_table_page_defs.h
  char occupied_[(BUCKET_ARRAY_SIZE - 1) / 8 + 1]; //array_某个位置是否被占领过
  // 0 if tombstone/brand new (never occupied), 1 otherwise.
  char readable_[(BUCKET_ARRAY_SIZE - 1) / 8 + 1]; //array_某个位置是否有数据
  // Do not add any members below array_, as they will overlap.
  MappingType array_[0];
  1. 首先说一下array_,它负责存储数据,这个数组在定义时声明为0,其实是一个小trick,目的是方便内存缓冲区的管理。每个HashTableBucketPage会分配4k内存,但是因为MappingType(是一个 std::pair)大小不同(可能是8/16/32/64字节不等),array_大小不好预分配,若申明为0则可灵活分配。
  2. readable意思是当前某个位置是否有数据可读,注意这里需要用位运算来确定,一个char有8位,所以readable_的长度是array_的1/8。比如如果要查询array的第10个元素是否有数据,则需要查看readable数组第10/8=1个元素的第10%8=2位元素。即(readable_[1] >> 2) & 0x01的值。
  3. occupied的意思是这个位置是否被占领过,这个是为了方便判定是否需要继续遍历bucket的。在插入时,我们遍历array,只需关注当前位置是否readable即可;在删除时,若发现一个位置是非occupied的,可以直接停止遍历。

这里我说一句题外话,使用occupied和readable是在linear probe里的策略,对unique key比较高效,但是本project要求支持non-unique key,occupied的作用反而小了些。
HashBucket Page的成员函数也比较简单,关键是做好array下标和readable、occupied的位置转换,这里涉及到位运算的置0、置1,相信读者们也没有太大问题。

3 ExtendibleHashTable

ExtendibleHashTable是一个重头戏,还是那句话,对于EXTENDIBLE HASH算法理解有困难的朋友可以在github看看demo实现,对完成Project极有帮助。这个类有三个函数需要重点关注,也是实现的重点:

  1. bool Insert(Transaction *transaction, const KeyType &key, const ValueType &value);
  2. bool Remove(Transaction *transaction, const KeyType &key, const ValueType &value);
  3. bool GetValue(Transaction *transaction, const KeyType &key, std::vector *result);

这三个函数不仅要完成对应的功能,还要实现并发安全性。对于前者来说,实现EXTENDIBLE HASH是关键;对于后者来说,一个最简单的方法就是在每个函数之前加锁,但是效率低;我自己的实现方案是利用分段锁加上一个double-check。下面说一下我是怎么做的。以插入(Insert)为例。

bool Insert(const KeyType &key, const ValueType &value) {
  根据key,从directory拿到key对应的bucket_page_id//分段锁提高了并发性,可以允许多个页同时被操作
  从buffer_manager_pool里fetch到bucket_page
  bucket_page->上锁 //需要保证只有一个线程修改页数据,
  根据key,从directory拿到key对应的bucket_page_id2 //double-check,这里是防止在前一行代码等待锁的过程中directory发生变化
  if (bucket_page_id == bucket_page_id2) { //目录页数据没变
     插入数据,若bucket_page已经满了,插入失败
  } 
  bucket_page->解锁
  if(bucket_page_满了) {
    split page
  }
  if (bucket_page满了 || 目录页数据变了) {
    Insert(key,value);//重新插入即可
  }
}

在GetValue和Remove里也使用了同样思路的分段锁+double-check的思路。

4 结果

2021 CMU-15445/645 Project #2 : Hash Index_第1张图片
通过的截图如上,总之这个Project还是比较难的,需要考虑的东西很多,不过收获也比较大,特别是考虑锁的粒度这一块。

你可能感兴趣的:(数据库,哈希算法,15445,数据库)