2021 CMU DB 15-445: [Project2] Hash Index

做了11天,思路不难,就是小错误恶心人。建议以后所有lab快点写出来,然后花3~4倍时间debug…

主要说一下3个部分写点啥,directory与bucket在整体哈希表中作用是什么,还有一些坑点。

hash_table_directory_page

一个directory是一个Page

directory_page的key-to-bucket_page_id原理

directory的作用是根据key的hash找到对应的bucket_page对象,再进行其他serach or insert otr remove操作。hash value截取后global_depth位就是bukcet_index,对应一个存放bucket_page_id的数组的下标,就是说key->Hash->bucket_index通过数组下标到值的映射就能得到bucket_page_id,最后通过buffer_pool_manager_能Fetch到bucket_page。

global_depth与local_depth作用

global_depth是最大的local_depth,用桶的local_depth才能得到桶真正对应的Hash值,若仅用global_depth会造成多个Hash对应同一个桶的ambigous情况。因为不同桶装的数据量不一样,比如仅用一个桶就能装下结尾为0的key-value数据,但需要2个桶来装下结尾为1的数据,这时虽然有00,01,10,11这4个桶,global_depth = 2,但00和10都是指向同一个存放结尾为0的bucket_page,这个bucket_page的local_depth = 1,所以桶真正对应的hash值其实是0,通过local_depth取,而不是00或01。

实际用到local_depth的地方是在insert和merge,详细的后面讲。例如insert_split中,改变bucket_idx到bucket_page_id的映射。例如原先标识桶(bucket_page)的Hash是xxx,现在这个桶装满了,分成2个桶装:0xxx与1xxx,这个重新分装的过程为reHash。此时就要通过local_depth获得标识桶的真实Hash值,即xxx,在此基础上加一位0或1来获得新的bucket_idx。可能原本这两个bucket_idx:0xxx与1xxx都是对应一个page_id,现在要改变映射到不同page_id。

hash_table_bucket_page

一个桶是一个Page

bucket_page存取数据原理

对桶进行数据操作,主要是insert与remove,isEmpty(判断Merge)与isFull(判断Split)会被其他类使用,桶存放数据的数据类型是一个存放Mappingtype的数组,大小为BUCKET_ARRAY_SIZE = 496,怎么判断有没有读到非法的位置呢?需要readable_数组标志数组的每一项是不是有存放数据,比如insert操作中在数组的第i位存放数据时,就要将readable数组的第i位置1,remove操作则将对应位置0,就在逻辑上删除了数据,occupied_数组则是如果在某个位置插入过数据会置1,从未插入过则为0,所以置为1后永远不会置为0,在初期用于检索数据时提前结束。这2个标志数组是一个char类型数组,大小为496 / 8,也就说char元素的每一位标志了数组中的一个位置。

至于为什么是这个数字,头文件中有详细说明,主要是为了在Page的Data部分(Size = 4096)reinterpreter_cast成BUCKET_PAGE时不报错,两者大小要一致,所以计算得出了这个大小。

每条key-value在桶中的存放位置是随机的,找某条key-value只能遍历。

Insert

之前没注意,单线程测试过了以为这块没什么问题,但并发测试会出现bucket存了2条一样key-vlaue值的问题,两种写法有细微的区别,Wrong Version是找到一个空位就跳出来,对相同k-v检测只到空位之前,即使bucket后面出现了相同的k-v值也不会被检测出来,Correct Version是要确定桶中没有相同k-v才返回free_pos。

在这里插入图片描述
这种细节真的很重要,很搞笑的是CMU单纯关于insert的测试用例,无论是单线程还是多线程都没测出来这个问题,只有到与remove一起的并发测试用例才报错,这时候就很难找问题,有可能是加锁问题,或者remove问题,总之很难,,,我找了2天,在各种锁的地方打log,但其实并不是锁的问题。。。
另外,真的要仔细看报错信息,res.size()=2还是能说明问题的,之前心态崩了,一直在做无用的debug,就算很麻烦也要好好看报错信息才能找到问题。。。

// Wrong Insert
int free_pos = -1;
 // LOG_INFO("full size %lld", static_cast<long long>(BUCKET_ARRAY_SIZE));
for (uint32_t i = 0; i < BUCKET_ARRAY_SIZE; i++) {
  if (IsReadable(i) && cmp(array_[i].first, key) == 0 && array_[i].second == value) {
    LOG_INFO("INSERT FASLE TYPE 1");
    return false;
  }
  if (!IsReadable(i)) {
    // LOG_INFO("%d is unReadable, can insert", i);
    free_pos = i;
    break;
  }
}
if (free_pos == -1) {
  LOG_INFO("INSERT FASLE TYPE 2");
  return false;
}
array_[free_pos] = MappingType(key, value);
SetReadable(free_pos);
SetOccupied(free_pos);
return true;
 
 // Correct Insert
int64_t free_slot = -1;
for (size_t i = 0; i < BUCKET_ARRAY_SIZE; i++) {
  if (IsReadable(i)) {
    if (cmp(key, array_[i].first) == 0 && value == array_[i].second) {
      // already existed the same key & value
      //                LOG_DEBUG("Same kv");
      return false;
    }
  } else if (free_slot == -1) {
    free_slot = i;
  }
}

if (free_slot == -1) {
  // is full
  LOG_DEBUG("Bucket is full");
  return false;
}

// insert it and return true
SetOccupied(free_slot);
SetReadable(free_slot);
array_[free_slot] = MappingType(key, value);
return true;

extendible_hash

小函数,类似如何取page_id这种在directory_page中已经说过了。

坑的是小函数封装的没什么用,比如FetchBucketPage返回HASH_TABLE_BUCKET_TYPE类型,他是Page类型的GetData()返回的一块内存,经过reinterpreter_cast转成的类型,但是如果要对这个page加读写锁,还是要用Page类型的对象啊。。所以小函数返回的东西没啥用。。。最后决定不用小函数了呗,全集成在大函数里。

insert

插入前判断一下待插入的桶是不是满了。如果满了,先分裂页面再插入,否则直接插入就行。
坑点是SplitInsert中要注意NewPage是不是空的!如果新页面不是空的,可能的原因是Project1写错了,,

Split_insert流程
在这里插入图片描述
简单来说,是先更新global_depth,然后更新directory中key的Hash到bucket_page_id的映射,最后将原来bucket中的数据重新映射到新的bucket_page。

判断要不要增加GD
在这里插入图片描述

注意要设置新的bucket_index对应的local_depth与bucket_page_id

重新映射bucket_index到bucket_page_id
在这里插入图片描述对bucket_idx前面加个1就是bucket_idx_split,偏移1<<local_depth设置相应的local_depth与bucekt_page_id

reHash
在这里插入图片描述对被分裂桶中的数据重新映射一下,插入到对应的bucket_page中

remove

先remove数据,删完后判断一下页面isEmpty() == true?是则merge,否则直接返回remove结果。
坑点是可能出现连续merge情况。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述一次删除页面导致多次merge,如local_depth=2的页面为空时,他merge的页面local_depth=3就不能merge,要当merge_page的local_depth=2时才能merge,所以merge_page的local_depth变小的时候会发生2次shrink,会直接使local_depth=1

concurrent-control

这个好像别的博客都不怎么写,我的写法是在取到page对象后立刻上锁,然后函数返回前解锁,如果函数对这个对象的成员有改写,加WLock(),否则加RLock()。注意在insert时,得到新page以后加立刻对新page加写锁,而remove时,delete页面前要解锁。

一般形式为:

table_latch_.RLock();
Page *directory_page_all = buffer_pool_manager_->FetchPage(directory_page_id_, nullptr);
directory_page_all->RWLatch();
HashTableDirectoryPage *dir_page = reinterpret_cast<HashTableDirectoryPage *>(directory_page_all->GetData());
page_id_t bucket_page_id = KeyToPageId(key, dir_page);
Page *bucket_page_all = buffer_pool_manager_->FetchPage(bucket_page_id, nullptr);
bucket_page_all->RWLatch();
HASH_TABLE_BUCKET_TYPE *bucket_page =
    reinterpret_cast<HashTableBucketPage<KeyType, ValueType, KeyComparator> *>(bucket_page_all->GetData());
    
...

bucket_page_all->RWUnlatch();
buffer_pool_manager_->UnpinPage(bucket_page_id, false);
directory_page_all->RWUnlatch();
buffer_pool_manager_->UnpinPage(directory_page_id_, false);
table_latch_.RUnlock();
return ;

坑点是函数跳转,一般会在函数跳转前把所有锁都解了,所有页面Unpin了,才进入另一个函数,那么进入另一个函数后,中间可能已经轮转到别的线程,因此,转移到新函数的条件可能不满足。例如,我假设满足x=1才进入NewFunc(),那么进入NewFunc()时,可能已经不满足x=1,因此要在新函数里开头处判断一下条件。

最后纪念一下啪~
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值