CMU 15445 数据库设计

hash

扩容的过程就是针对每一个哈希值重新对第一维数组容量取余。
假设容量从8增加到16,那么原来3号槽位(011)保存的哈希值3(0x0011)和11(0x1011)被各自分配到3号和11号槽位。
特性: 如果采用高位进位的顺序遍历槽位,假设当前遍历到 110 这个槽位,这时从容量8扩容到容量16后,110槽位上所有的元素对应的新槽位是 0110 或1110,是相邻的,并且0110前的所有槽位在容量8时已经遍历完了

在这里插入图片描述
取余也可以理解为对二进制哈希值进行截取,比如截取i位作为槽位,导致多个哈希值对应一个槽位。另外可以让多个槽位对应一个桶,做法是继续对槽位进行截取,假设截取位数为 i j i_j ij,那么最多有 2 ( i − i j ) 2^(i-i_j) 2(iij)个槽位对应一个桶j。

插入导致桶j超过容量后,如果i= i j i_j ij, 说明一个槽位对应一个桶,这时必须把第一位数组的容量翻倍,并且使i=i+1;

当 i> i j i_j ij 说明此时桶j对应多个槽位,此时不需要更新整个一位数组,只需要把桶j分成两个桶j和z, 且新 i j i_j ij i z i_z iz等于旧 i j i_j ij+1。

  • 判断元素是否存在与hash表中,一个思路是使用bloom 过滤器,但是其不支持删除操作,因为(位图中一个bit位被多个元素公用,且不能确定被删除的元素是否在位图中)

  • C++实现

    void ExtendibleHash<K, V>::Insert(const K &key, const V &value) {
          
          
            _mtx.try_lock();
            Node<K,V>* newNode=new Node<K,V>(key,value);
            size_t h=HashKey(key);
    //        std::cout<<"insert"<<h<<" "<<std::endl;
            size_t cutH=CutBits(h,_numBits);
            std::shared_ptr<BucketList<K,V>> sharedBucket=_table[cutH];
            BucketList<K,V>* bucket = sharedBucket.get();
            Node<K,V>& result=bucket->_Find(key);
            if(result.next!=NULL){
          
          
                result.pre->next=newNode;
                result.next->pre=newNode;
                newNode->pre=result.pre;
                newNode->next=result.next;
                delete &result;
            }else{
          
          
                bucket->PushBuck(*newNode);
                while(bucket->_count>bucket->_capacity){
          
          
                    if(bucket->_numBits==_numBits){
          
          
                        DoubleSize();
                        SplitBucket(bucket);
                    }else{
          
          
                        SplitBucket(bucket);
                    }
                }
            }
            
    
    template<typename K, typename V>
    void ExtendibleHash<K, V>::DoubleSize() {
          
          
       std::shared_ptr<BucketList<K,V>>* newTable=new std::shared_ptr<BucketList<K,V>>[1<<(_numBits+1)];
       for(int i=0;i<(1<<_numBits);i++){
          
          
           newTable[i<<1]=_table[i];
           newTable[(i<<1)+1]=_table[i];
       }
       _numBits++;
    //    std::cout<<"double"<<" ";
       delete [] _table;
       _table=newTable;
    }
    
    template<typename K, typename V>
    void ExtendibleHash<K, V>::SplitBucket(BucketList<K, V> *bucket) {
          
          
    //    std::cout<<"split"<<" ";
       int oldBits=bucket->_numBits;
       size_t  oldIndex=bucket->_tableIndex;
       BucketList<K,V>* newBucket=new BucketList<K,V>(oldBits+1,(oldIndex<<1)+1,BUCKET_LEN);
       bucket->_numBits=oldBits+1;
       bucket->_tableIndex=oldIndex<<1;
       for(Node<K,V>* cur=bucket->head.next;cur->next!=NULL;){
          
          
           size_t h=HashKey(cur->_key);
           auto next=cur->next;
           if(EndWithOne(h,oldBits+1)){
          
          
               bucket->UnLink(*cur);
               newBucket->PushBuck(*cur);
    //            std::cout<<"Trans"<<cur->_key<<" "<<std::endl;
           }
           cur=next;
       }
       int sub=_numBits-oldBits;
       std::shared_ptr<BucketList<K,V>> newShared_ptr=std::shared_ptr<BucketList<K,V>>(newBucket);
       for(size_t i=oldIndex<<sub;i<((oldIndex+1)<<sub);i++){
          
          
           if(((i>>(sub-1))&1)==1){
          
          
               _table[i]=newShared_ptr;
           }
       }
       return ;
    
    }
    

    DoubleSize用于将一位数组的容量翻倍,SplitBucket用于将某一个桶分裂。
    实现过程需要注意遍历链表时对链表元素进行删除的情况,会导致找不到下一个元素。

储存和索引

  • LRU (least recently used )
    维护一个链表,假设链表节点node储存的值为v, 以v为key,node为value创建一个hash表。删除链表首节点的同时删除hash表对应的元素。如果要删除任意v值,首先通过hash表找到对应的节点,然后从链表中删除。

  • bufferPool

    Page *BufferPoolManager::FetchPage(page_id_t page_id) {
          
          
       Page* result;
       bool flag=page_table_->Find(page_id,result);
       if(flag){
          
          
           result->pin_count_++;
           return result;
       }else{
          
          
           if(!(free_list_->empty())){
          
          
               result=free_list_->front();
               free_list_->pop_front();
               disk_manager_->ReadPage(page_id,result->GetData());
               result->pin_count_++;
               result->page_id_=page_id;
               page_table_->Insert(page_id,result);
               return result;
           }else{
          
          
               if(replacer_->Victim(result)){
          
          
                   if(result->is_dirty_){
          
          
                       disk_manager_->WritePage(result->page_id_,result->GetData());
                       page_table_->Remove(result->GetPageIvcd());
                       disk_manager_->ReadPage(page_id,result->GetData());
                       result->pin_count_++;
                       result->page_id_=page_id;
                       page_table_->Insert(page_id,result);
                       return result;
                   }
               }
           }
       }
       return nullptr;
    }
    

    以page单位管理内存到磁盘的映射。磁盘中的每个page有一个page_id。 给一定数量的磁盘page分配了内存Page。 用户想访问某个id对应的磁盘page时,可以转而访问对应的内存Page。两者的映射保存在HashTable<page_id_t, Page *>。 freelist中保存的是尚未分配的内存Page,Replacer<Page *> 保存的是hashtable中标记次数为0的内存page(有点类似于引用计数)。如果freelist不够的时候就从replacer中按照LRU选出一个内存page,把它写回磁盘(s_dirty标记决定是否写回)后分配给新的磁盘page。

  • 使用列式储存的好处,方便cpu进行缓存,提高压缩效率,减少i/o次数,因为可以不用取回不需要的列,方便cpu进行向量化处理。

  • 行式储存中,一条记录存在一个tuple中,各个列的值value被拼接在一起。对于变长的value,例如字符串,在value前面保存它的长度。首先用变长value在tuple中的偏移量替代它们并把它们和固定长度的value拼接在一起,然后紧接着保存变长value的具体值。

  • tuple在一个page中的储存形式:首先保存元信息,比如page_id,next_page_id,pre_page_id,tuple数量以及空闲空间起始偏移位置,之后是各个tuple的偏移以及长度。然后从后往前保存各个tuple的数据。每个tuple在插入到page的时候分配了一个固定的rid,由page_id和slot_num组成,可以用slot_num来检索元信息。删除某个tuple时,需要将偏移量在它之前的所有tuple往后移以填补它的空缺,保留其slot_num,但是需要将其在元信息中的偏移和长度置零。在下次插入的时候,可以重复利用这个slot_num, 然后在空闲空间分配新的空间。也就是说,slot_num的顺序并不代表tuple在空间上的顺序。

  • table_heap负责管理和连接page, 当往当前cur_page插入一个tuple时,如果空间不够,table_heap会访问cur_page中记录的下一个page,直到找到一个拥有足够空间的。如果此时cur_page没有next_page记录,table_heap会从buffer_pool中new一个给它。插入一个tuple,page的引用计数会加1,反之会减1。初始的cur_page对应table_heap中的first_page_id。table_heap还负责把这些用于储存page的table_page用pre_page_id和next_page_id连接起来。

  • table_iterator 负责遍历table_heap管理的所有page里的tuple.

  • b+树的每个page中用一个array来保存key/value对,internal_page中array[0]的key是没用的,而leaf_page中array[0]的key是有用的。

  • insert实现
    b+树中每一个节点占用一个page,由于叶子节点储存的value是RID类型,内部节点储存的value是pafe_id类型,所以需要分开实现两个类。然后用一个tree类型来管理这些节点(申请和使用page),并实现插入操作。直接把键值对插入叶子节点,和因上溢把键值插入内部节点所引发的后续操作不同,对此在tree类型中分别实现两个成员函数InsertIntoLeaf和InsertToParentWithIndex。每次insert操作,先调用InsertIntoLeaf, 其内部如果发生上溢会调用InsertToParentWithIndex,如果内部节点也发生了上溢,会递归的调用InsertToParentWithIndex。
    坑点:对于内部节点,分裂后转移到新节点上的子节点,需要修改他们的父节点为新节点。 对于叶子节点,分裂后需要分别更新老节点和新节点的next_page_id属性。内部节点的键值对数量必须大于等于2,叶子节点的键值对数量必须大于等于1,这样才能保证按键值检索的正确性。节点包含键值对数量的最大值应该考虑,插入时溢出,但是还未进行分裂的情况,所以需要预留一个空位。

  • delete 实现
    当节点内键值对数量大于等于最大容量的1/2时,称其满足填充因子。在删除过程中,一层内只可能有一个节点不满足填充因子,所以不满足填充因子的节点一定可以从其邻居节点借一个节点从而满足,或者和邻居节点合并。没有邻居节点的节点一定是根节点。坑点:合并节点的时候使用MoveAllTo函数把当前节点的剩余键值对全部移到左邻居或右邻居。和分裂时使用的MoveHalfTo不同,这里不是把关键值移到一个新节点,而是插入已经有关键值存在的左右邻居。所以针对左右邻居,需要考虑从末尾插入和从开头插入的不同。删除根节点后,除了更新树的根节点外,还需要更新其子节点的父节点为INVALID_PAGE_ID。通过一个unpin(page_id)函数来通知buffer_pool把第page_id页加入LRU队列。除了手工调用unpin之外,还可以通过shared_ptr在page退出作用域是自动调用unpin。做法是初始化shared_ptr时同时传入一个deleter对象:

    class Deleter {
          
          
    public:
        explicit Deleter(BufferPoolManager * buffer_pool_manager){
          
          
            buffer_pool_manager_=buffer_pool_manager;
        }
        void operator ()(BPlusTreePage* page) {
          
          
            std::cout<<"unpin__"<<page->GetPageId()<<std::endl;
            buffer_pool_manager_->UnpinPage(page->GetPageId(),true);
        }
    
    private:
        BufferPoolManager *buffer_pool_manager_;
    };
    

    从buffer_pool中申请新page之后,一定要清空里面的data.

  • LRUreplacer
    LRUreplacer是buffer_pool的成员,其负责LRU算法的实现,其内部维护一个链表,pre和next等指针全部是智能指针,为了防止析构时发生死锁,先正序遍历一次把next指针清空,然后倒序把pre指针清空。

并发控制

  • b+树并发
    按照课程提供的文件,每个page有两个锁,writer_entered_属性记录当前页上是否有写锁,reader_count_记录page上读锁的数量。读锁有一个最大数量。在申请写锁时,必须满足writer_entered_为false和reader_count_==0两个条件,并且分别用while语句包围起来,一但不满足其中的一个就会利用条件变量进入阻塞状态。
    有以下几种情况需要唤醒等待的信号量,释放读锁时,当读锁数量为0时唤醒一个申请写锁的线程,当读锁数量为最大值减1时唤醒一个申请读锁的线程,释放写锁时,唤醒所有申请读锁和写锁的进程。 除开第一种情况,其它情况共用一个信号量。

    void WLock() {
          
          
       std::unique_lock<mutex_t> lock(mutex_);
       while (writer_entered_ || reader_count_ > 0 || crab_entered_){
          
          
           if(writer_entered_ ){
          
          
               writer_release_.wait(lock);
           }
    
           if(crab_entered_){
          
          
               crab_release_.wait(lock);
           }
           if(reader_count_ > 0){
          
          
               reader_to_zero_.wait(lock);
           }
    
       }
       writer_entered_ = true;
     }
    
     void WUnlock() {
          
          
    
       std::lock_guard<mutex_t> guard(mutex_);
    //      static long lock_count=0;
    //      std::cout<<std::this_thread::get_id()<<" "<<lock_count++<<std::endl;
       assert( writer_entered_ );
       writer_entered_ = false;
       writer_release_.notify_all();
     }
    
     void RLock() {
          
          
       std::unique_lock<mutex_t> lock(mutex_);
       while (writer_entered_ || reader_count_ == max_readers_){
          
          
           if(writer_entered_){
          
          
               writer_release_.wait(lock);
           }
           if(reader_count_ == max_readers_){
          
          
               reader_to_max_sub_1.wait(lock);
           }
       }
       reader_count_++;
     }
    
     void RUnlock() {
          
          
       std::lock_guard<mutex_t> guard(mutex_);
       assert(reader_count_>0);
       reader_count_--;
       if (reader_count_ == 0) {
          
          
           reader_to_zero_.notify_all();
       }
       if (reader_count_ == max_readers_ - 1){
          
          
           reader_to_max_sub_1.notify_one();
       }
    
     }
    
       void CLock() {
          
          
           std::unique_lock<mutex_t> lock(mutex_);
    
           while (writer_entered_ || crab_entered_){
          
          
               if(writer_entered_ ){
          
          
                   writer_release_.wait(lock);
               }
               if(crab_entered_){
          
          
                  crab_release_.wait(lock);
               }
           }
           crab_entered_=true;
       }
    
       void CUnlock() {
          
          
           std::lock_guard<mutex_t> guard(mutex_);
           assert(crab_entered_);
           crab_entered_= false;
           crab_release_.notify_all();
       }
    
       void TransCrab(){
          
          
           std::unique_lock<mutex_t> lock(mutex_);
           assert(crab_entered_ && !writer_entered_);
           while(reader_count_>0){
          
          
               reader_to_zero_.wait(lock);
           }
           writer_entered_= true;
           crab_entered_= false;
           crab_release_.notify_all();
     }
     bool CheckFree(){
          
          
         return !crab_entered_ && !writer_entered_ && reader_count_ == 0;
     }
    

    当使用蟹行锁后,需要增加一个可变锁,它和读锁相容但和自身以及写锁不相容,所以可变锁释放时需要唤醒申请写锁和其它可变锁的线程。所有需要唤醒的情况使用各自单独的信号量,为了防止等待第二个条件的时候,本来已经满足的第一条件又被其它线程修改,所以把所有条件判断语句写在一个while里。使用自带的header_page当做根节点的前哨节点,要读取或修改根节点时必须先获取前哨节点的锁,这样做可以把根节点和其它内部以及叶节点的操作统一起来。当前线程获得某一节点的写锁后,马上把这个节点压入线程对应的transaction中的队列里,等到当前线程的插入或删除操作完成后再按压入顺序的相反顺序释放锁。

    当插入一颗空树时,为了避免直接加锁降低并发性,先检查树是否为空,之后再获取全局锁。 获取锁之后,再检查一次树是否为空,如果不为空就直接返回,否则修改根节点。

    if(IsEmpty()){
          
          
        rootLatch_.lock();
        if(!IsEmpty()){
          
          
            rootLatch_.unlock();
            return false;
        }else{
          
          
            auto page = buffer_pool_manager_->FetchPage(HEADER_PAGE_ID);
            page->WLatch();
            StartNewTree(key,value,transaction);
            page->WUnlatch();
            buffer_pool_manager_->UnpinPage(HEADER_PAGE_ID,true);
            rootLatch_.unlock();
            return true;
        }
    
    }else{
          
          
        return false;
    }
    

    坑点: 锁释放的时候使用notify_all()更加保险,确保所有需要唤醒的线程能够接收到信号,否则陷入死锁。REMOVE函数中对某一节点的deletepage操作需要留到该节点的写锁释放后再进行。为了防止写锁释放后又有其它线程获得锁,保留transaction队列中的第一个元素(所有获得写锁节点中高度最高的),等到所有deletepage操作完成之后在释放该节点的写锁:

        page_id_t predecessor;
    int PageSetSize=transaction->GetPageSet()->size();
    for(int i=0;i<PageSetSize-1;i++){
          
          
        predecessor=transaction->GetPageSet()->back()->GetPageId();
        transaction->GetPageSet()->back()->WUnlatch();
        transaction->GetPageSet()->pop_back();
        buffer_pool_manager_->UnpinPage(predecessor, false);
    }
    for(auto it=transaction->GetDeletedPageSet()->begin();
        it!=transaction->GetDeletedPageSet()->end();it++){
          
          
        buffer_pool_manager_->DeletePage(*it);
    }
    assert(transaction->GetPageSet()->size()==1);
    predecessor=transaction->GetPageSet()->back()->GetPageId();
    transaction->GetPageSet()->back()->WUnlatch();
    transaction->GetPageSet()->pop_back();
    assert(transaction->GetPageSet()->empty());
    buffer_pool_manager_->UnpinPage(predecessor, false);
    

    remove函数中,第一次判断树不为空的结果可能其他线程修改,要等到真正拿到哨兵节点的锁后再判断一次。调用delete删除某页时,该页可能正好处在被其它线程释放锁,但是没有unpin的状态。

锁管理

lock_manager负责处理每个事务的共享锁申请,排他锁申请,锁升级和解锁操作。使用的方法是每个事务所在的线程,调用lock_manager的函数,并且传入事务指针以及元组的索引对象rid。 lock_manager内部维护一个把rid映射到事务队列的map,在每个队列中既有等待锁的事务,也有已经申请到锁的事务。每个队列中第一个事务一定是已近申请到锁的事务,如果第一个事务申请的是共享锁,那么从它开始到第一个申请排他锁的事务之前,所有的事务都是已经申请到锁的。 为了区分每个事务对应某个rid到底申请的是共享还是排他锁,在transaction头文件中定义两个集合,分别表示该事务申请的排它锁和共享锁对应的rid的集合。为了方便单独阻塞和唤醒每个等待锁的事务,在transaction头文件中定义条件变量。为了满足2PL二段锁协议,申请锁时事务应处在growing状态,否则abort当前事务。在锁升级时如果当前事务已经拥有或在等待排他锁,也会引发abort。 在abort之前,需要释放改transaction拥有和等待的所有锁,并且唤醒等待这些锁的其它事务。

注意:必须通过引用的方式从map中拿到事务队列。在升级锁时,只有当前事务在队首,并且第二个事务申请的是排它锁或者没有第二个事务时,才能在原地将当前事务的锁升级为排它锁,否则需要将事务移到第一个申请排它锁的事务之前。

  • unlock 函数:

    bool LockManager::Unlock(Transaction *txn, const RID &rid) {
          
          
        std::unique_lock<mutex_t> lock(mutex_);
        if(strict_2PL_){
          
          
            if(txn->GetState()!=TransactionState::COMMITTED){
          
          
                Release(txn);
                txn->SetState(TransactionState::ABORTED);
                return false;
            }
        }else{
          
          
            if(txn->GetState()==TransactionState::GROWING){
          
          
                txn->SetState(TransactionState::SHRINKING);
            }
            if(txn->GetState()!=TransactionState::SHRINKING &&
               txn->GetState()!=TransactionState::COMMITTED ){
          
          
                Release(txn);
                txn->SetState(TransactionState::ABORTED);
                return false;
            }
        }
        auto it=rid_to_queue.find(rid.Get());
        if(it!=rid_to_queue.end()){
          
          
            std::list<Transaction *>& wait_queue=it->second;
            auto queue_it=wait_queue.begin();
            auto first=*queue_it;
            auto second=*(++queue_it);
            if(txn->GetExclusiveLockSet()->find(rid)!=txn->GetExclusiveLockSet()->end()){
          
          
                // exclusive lock  which ready to unlock  must  be the first element in the queue
                assert(first==txn);
                txn->GetExclusiveLockSet()->erase(rid);
                wait_queue.pop_front();
                for(;queue_it!=wait_queue.end();queue_it++){
          
          
                    auto cur=*queue_it;
                    if(cur->GetExclusiveLockSet()->find(rid)!=cur->GetExclusiveLockSet()->end()){
          
          
                        cur->lock_available_.notify_one();
                        break;
                    }else{
          
          
                        cur->lock_available_.notify_one();
                    }
                }
                return true;
            }else{
          
          
                if(txn==first && wait_queue.size()> 1 && (second->GetExclusiveLockSet()->find(rid)
                                                           !=second->GetExclusiveLockSet()->end())){
          
          
                    txn->GetSharedLockSet()->erase(rid);
                    wait_queue.pop_front();
                    second->lock_available_.notify_one();
                }else{
          
          
                    txn->GetSharedLockSet()->erase(rid);
                    wait_queue.remove(txn);
                }
                return true;
            }
        }
    }
    

    调用unlock时,如果当前是非严格两段s锁,如果当前状态是growing, 需要修改为shrinking,这样线程在之后申请其它锁时会触发abort。 如果当前是严格两段式锁,则当前状态必须是committed。

  • deadlock prevention

    使用wait-die 协议防止死锁发生, 按照每个事务的id确定优先级,越早开始的事务优先级越高,只能优先级高的事务去抢占优先级低的事务,否则引起abort。
    坑点: 原本的想法是只和wait_queue中已经申请到锁的事务进行优先级对比,这样做的后果是队列中下一个事务获得锁后可能会违反wait-die协议, 待加入队列的阻塞事务必须比队列中的所有事务有更高的优先级。

logManager

TablePage::UpdateTuple(const Tuple &new_tuple, Tuple &old_tuple,
                            const RID &rid, Transaction *txn,
                            LockManager *lock_manager,
                            LogManager *log_manager)

这个函数负责真正将新的tuple更新到page中去。 首先把page中保存的旧tuple的内容复制到old_tuple对象中,然后把新tuple的内容写到旧tuple对应的位置上,因为两者大小可能不同,所以要移动所有旧tuple之前的tuple,并更新他们的tuple_offset.

TablePage::MarkDelete 不会真正移除rid对应的tuple,而是把它的tuple_size字段设为负数。
TablePage::RollbackDelete 重新把tuple的size设置为正数
TablePage::ApplyDelete 真正删除rid对应的tuple,把它的tuplesize设置为0.。 并移动它之前的所有tuple. 只有在commit时,或者回滚insert操作时才会调用。
TablePage::InsertTuple: 先找到一个对应tuple_size为0的slot_num,如果找不到就把新tuple的slot_num设置为TupleCount的值。但是不管给新tuple分配的slot_num是多少,新tuple的储存位置还是在该page的freespace里。
使用applydelete和markdelete的原因是可以把删除操作变成是幂等(idempotent)的。

只有在TransactionManager::Commit时才会调用TablePage::ApplyDelete。

logmanager利用后台线程,每隔LOG_TIMEOUT就把log fush到磁盘。它会维护一个persistentLSN,表示已经写入磁盘的最新LSN。
有两种情况会强制flush, 第一种情况是当buffer pool回收某一个page时,比较page上的pageLSN和persistentLSN, 如果pageLSN更大的话就需要强制flush。 第二种是当txnManager调用Abort或者Commit时。第三种情况是追加log时发现logbuffer已满。
这里的强制flush指的是等待log_timeout或着通过信号量触发logManager的写入磁盘操作。

每个事务自己会维护一个队列write_set,保存执行的所有写操作。 当事务自己调用abort时,会回滚队列中的所有操作。这些回滚操作,除了applyDelete和rollbackDelte都会和正常操作一样保存到write set,所以在abort过程中相同的delete操作不会多次执行,delete也不会引起多次相同的insert操作(但实际上abort执行过程中不会嵌套执行abort,并且在redo中通过pageLSN也可以防止重入现象)
在undo操作中,遇到applyDelete应该视情况回滚,如果此时对应tuple在page_table中的size为0,说明applydelete已经执行成功,这时应该在old_rid上执行insert。 如果size不为0,那直接跳过这条record. 不能要求insert操作一定要发生在原来的槽位上,假设事务T1删除了槽位3上的item, 然后事务T2利用空的槽位3插入了一条新的item,并且commit。 在undo阶段时,由于t2已经提交所以不用回滚,而T1需要回滚,此时已经不可能再插入到槽位3。

每个record 根据操作的不同长度也会不同,比如update对应的record要保存两个tuple,所以在保存record时,在开头保存当前record的长度。LSN记录的是record的序列号,从LSN到record在log文件中偏移的映射,需要在redo的过程中建立。

纯数据形式的tuple保存在table_page, tuple类型提供了SerializeTo和DeserializeFrom方便向recod中写入和读取tuple。

由于使用的是严格两段式锁,所以undo时直接按照各个txn执行即可。

newpage操作同样也要利用record记录,在redo时如果record的LSN大于对应磁盘上的pageLSN,则需要利用FetchPage拿到这一页,然后重新设置它的pre_page_id和next_page_id。disk_manager中没有实现page的回收,只是用地增量next_page_id指示下一个空闲页的位置。 first_page_id是table_heap的一个属性,表示table_heap中的第一个page的id,insert操作首先会在这个page上搜索空闲槽。 系统崩溃恢复table_heap是必须提供first_page_id.

在redo阶段碰到一条newPage对应的record,如果此时diskmanager中不存在此page(因为diskmanager可以在db文件的任何位置读写,所以这种情况不存在)则使用bufferManager的newpage进行申请,进而更新bufferManager的next_page_id属性。 如果diskManager中存在此page就直接++next_page_id。因为不使用checkpoint,所以redo后会保证buffermanager的next_page_id和系统崩溃前保持一致。 最好的方式是newpage对应的record记录当前申请页的page_id和pre_page_id。

在往log中append一条record时,先判断log_buffer会不会溢出,如果会就交换log_buffer和flush_buffer,并强制flush。

  • log_manager 实现
void LogManager::RunFlushThread() {
    
    
    if(ENABLE_LOGGING){
    
    
        return;
    }else{
    
    
        ENABLE_LOGGING=true;
        flush_thread_=new std::thread([&] {
    
    
            std::unique_lock<std::mutex> lock(latch_,std::defer_lock);
            while(ENABLE_LOGGING){
    
    
                lock.lock();
                cv_.wait_for(lock,LOG_TIMEOUT);
                std::swap(log_buffer_,flush_buffer_);
                std::swap(buffer_offset_,flush_offset_);
                lock.unlock();
                append_cv_.notify_one();
                disk_manager_->WriteLog(flush_buffer_,flush_offset_);
                flush_offset_=0;
                persistent_lsn_.store(last_append_lsn_);
                commit_cv_.notify_one();
            }
        });
    }

}
/*
 * Stop and join the flush thread, set ENABLE_LOGGING = false
 */
void LogManager::StopFlushThread() {
    
    
    std::unique_lock<std::mutex> lock(latch_);
    ENABLE_LOGGING= false;
    cv_.notify_one();
    commit_cv_.wait(lock);
    flush_thread_->join();
    delete flush_thread_;
}

lsn_t LogManager::AppendLogRecord(LogRecord &log_record) {
    
    
    std::unique_lock<std::mutex> lock(latch_);
    log_record.lsn_=next_lsn_++;
    while(log_record.GetSize()+buffer_offset_>LOG_BUFFER_SIZE){
    
    
        cv_.notify_one();
        append_cv_.wait(lock);
    }
    memcpy(log_buffer_ + buffer_offset_, &log_record, LogRecord::HEADER_SIZE);
    int pos=buffer_offset_+LogRecord::HEADER_SIZE;
    if(log_record.log_record_type_==LogRecordType::INSERT){
    
    
        memcpy(log_buffer_+pos,&log_record.insert_rid_,sizeof(RID));
        pos+=sizeof(RID);
        log_record.insert_tuple_.SerializeTo(log_buffer_+pos);
    }else if(log_record.log_record_type_==LogRecordType::APPLYDELETE||
            log_record.log_record_type_==LogRecordType::ROLLBACKDELETE ||
            log_record.log_record_type_==LogRecordType::MARKDELETE){
    
    
        memcpy(log_buffer_+pos,&log_record.delete_rid_,sizeof(RID));
        pos+=sizeof(RID);
        log_record.delete_tuple_.SerializeTo(log_buffer_+pos);
    }else if(log_record.log_record_type_==LogRecordType::UPDATE) {
    
    
        memcpy(log_buffer_ + pos, &log_record.update_rid_, sizeof(RID));
        pos += sizeof(RID);
        log_record.old_tuple_.SerializeTo(log_buffer_ + pos);
        pos += log_record.old_tuple_.GetLength();
        log_record.new_tuple_.SerializeTo(log_buffer_ + pos);
    }else if(log_record.log_record_type_==LogRecordType::NEWPAGE){
    
    
        memcpy(log_buffer_+pos,&log_record.prev_page_id_,sizeof(page_id_t));
    }

    buffer_offset_+=log_record.GetSize();
    last_append_lsn_.store(log_record.GetLSN());
    return log_record.GetLSN();

logmanager 利用单独的线程在后台将通过:AppendLogRecord添加的record写入磁盘,有两种方式可以唤醒线程并进行一次写入,分别是通过通过通知条件变量cv_和经过固定的log_timeout时间。使用双缓冲log_buffer和flush_buffer的好处是,只要swap操作完成,不用等到log真正落盘就可以通过append_cv_通知条用AppendLogRecord的其它线程继续运行。 等真正落盘后再更新log_manager的persistent_LSN, 并通过commit_LSN通知调用commit或abort操作的其它线程。虽然log不一定会填满整个log_buffer,但落盘时log之间是不会有间隙的。

坑点:为了实现双缓冲,必须同时实现buffer_offset和flush_offset,用来记录buffer中从开头到何处需要写入磁盘。

  • redo/undo 实现
bool LogRecovery::DeserializeLogRecord(const char *data,
                                             LogRecord &log_record) {
    
    
    size_t cur_offset=data-log_buffer_;
    if(data+LogRecord::HEADER_SIZE>LOG_BUFFER_SIZE+log_buffer_){
    
    
        return false;
    }
    memcpy(&log_record,data,LogRecord::HEADER_SIZE);
    if(data+log_record.size_>LOG_BUFFER_SIZE+log_buffer_
    || log_record.size_<LogRecord::HEADER_SIZE){
    
    
        return false;
    }
    data+=LogRecord::HEADER_SIZE;
    if(log_record.log_record_type_==LogRecordType::INSERT){
    
    
        memcpy(&(log_record.insert_rid_),data,sizeof(RID));
        data+= sizeof(RID);
        log_record.insert_tuple_.DeserializeFrom(data);
    }else if(log_record.log_record_type_==LogRecordType::APPLYDELETE||
             log_record.log_record_type_==LogRecordType::ROLLBACKDELETE ||
             log_record.log_record_type_==LogRecordType::MARKDELETE){
    
    
        memcpy(&(log_record.delete_rid_),data,sizeof(RID));
        data+= sizeof(RID);
        log_record.delete_tuple_.DeserializeFrom(data);
    }else if(log_record.log_record_type_==LogRecordType::UPDATE){
    
    
        memcpy(&(log_record.update_rid_),data,sizeof(RID));
        data+= sizeof(RID);
        log_record.old_tuple_.DeserializeFrom(data);
        data+=log_record.old_tuple_.GetLength()+ sizeof(int32_t);
        log_record.new_tuple_.DeserializeFrom(data);
    }else if(log_record.log_record_type_==LogRecordType::NEWPAGE){
    
    
        log_record.prev_page_id_ = *reinterpret_cast<const page_id_t *>(data);
        log_record.cur_page_id= *reinterpret_cast<const page_id_t *>(data + sizeof(page_id_t));
    }
    log_record.UpdateSize();

    return true;
}

/*
 *redo phase on TABLE PAGE level(table/table_page.h)
 *read log file from the beginning to end (you must prefetch log records into
 *log buffer to reduce unnecessary I/O operations), remember to compare page's
 *LSN with log_record's sequence number, and also build active_txn_ table &
 *lsn_mapping_ table
 */
void LogRecovery::Redo() {
    
    
    global_offset_=0;
    buffer_offset_=0;
    while(disk_manager_->ReadLog(log_buffer_,LOG_BUFFER_SIZE,
                                 global_offset_)){
    
    
        while(true) {
    
    
            LogRecord log;
            bool isPageModified = false;
            TablePage *page;
            if (DeserializeLogRecord(log_buffer_ + buffer_offset_, log)) {
    
    
                active_txn_[log.GetTxnId()] = log.GetLSN();
                lsn_mapping_[log.GetLSN()] = make_pair(global_offset_ + buffer_offset_, log.size_);
                if (log.log_record_type_ == LogRecordType::INSERT) {
    
    
                    auto rid = log.insert_rid_;
                    page = static_cast<TablePage *>(
                            buffer_pool_manager_->FetchPage(rid.GetPageId()));
                    assert(page != nullptr);
                    if (log.GetLSN() > page->GetLSN()) {
    
    
                        RID tmp_rid;
                        page->InsertTuple(log.insert_tuple_, tmp_rid, nullptr, nullptr, nullptr);
                        isPageModified = true;
                    }
                } else if (log.log_record_type_ == LogRecordType::APPLYDELETE) {
    
    
                    auto rid = log.delete_rid_;
                    page = static_cast<TablePage *>(
                            buffer_pool_manager_->FetchPage(rid.GetPageId()));
                    assert(page != nullptr);
                    if (log.GetLSN() > page->GetLSN()) {
    
    
                        RID tmp_rid;
                        page->ApplyDelete(rid, nullptr, nullptr);
                        isPageModified = true;
                    }
                } else if (log.log_record_type_ == LogRecordType::MARKDELETE) {
    
    
                    auto rid = log.delete_rid_;
                    page = static_cast<TablePage *>(
                            buffer_pool_manager_->FetchPage(rid.GetPageId()));
                    assert(page != nullptr);
                    if (log.GetLSN() > page->GetLSN()) {
    
    
                        RID tmp_rid;
                        page->MarkDelete(rid, nullptr, nullptr, nullptr);
                        isPageModified = true;
                    }
                } else if (log.log_record_type_ == LogRecordType::ROLLBACKDELETE) {
    
    
                    auto rid = log.delete_rid_;
                    page = static_cast<TablePage *>(
                            buffer_pool_manager_->FetchPage(rid.GetPageId()));
                    assert(page != nullptr);
                    if (log.GetLSN() > page->GetLSN()) {
    
    
                        RID tmp_rid;
                        page->RollbackDelete(rid, nullptr, nullptr);
                        isPageModified = true;
                    }
                } else if (log.log_record_type_ == LogRecordType::UPDATE) {
    
    
                    auto rid = log.update_rid_;
                    page = static_cast<TablePage *>(
                            buffer_pool_manager_->FetchPage(rid.GetPageId()));
                    assert(page != nullptr);
                    if (log.GetLSN() > page->GetLSN()) {
    
    
                        RID tmp_rid;
                        page->UpdateTuple(log.new_tuple_,
                                          log.old_tuple_, rid, nullptr, nullptr, nullptr);
                        isPageModified = true;

                    }
                } else if (log.log_record_type_ == LogRecordType::NEWPAGE) {
    
    
                    page = static_cast<TablePage *>(
                            buffer_pool_manager_->FetchPage(log.cur_page_id));
                    assert(page != nullptr);
                    if (log.GetLSN() > page->GetLSN()) {
    
    
                        page->Init(log.cur_page_id, PAGE_SIZE, log.prev_page_id_, nullptr, nullptr);
                        if (log.prev_page_id_ != INVALID_PAGE_ID) {
    
    
                            auto pre_page = static_cast<TablePage *>(
                                    buffer_pool_manager_->FetchPage(log.prev_page_id_));
                            pre_page->SetNextPageId(log.cur_page_id);
                            buffer_pool_manager_->UnpinPage(log.prev_page_id_, true);
                        }
                        isPageModified = true;
                    }
                } else if(log.log_record_type_ == LogRecordType::COMMIT ||
                           log.log_record_type_ == LogRecordType::ABORT) {
    
    
                    active_txn_.erase(log.GetTxnId());
                }

                if (log.log_record_type_ != LogRecordType::BEGIN &&
                    log.log_record_type_ != LogRecordType::COMMIT &&
                    log.log_record_type_ != LogRecordType::ABORT) {
    
    
                    if(isPageModified){
    
    
                        page->SetLSN(log.GetLSN());
                    }
                    buffer_pool_manager_->UnpinPage(page->GetPageId(), isPageModified);
                }

                buffer_offset_+=log.size_;
            } else {
    
    
                if(buffer_offset_==0){
    
    
                    return;
                }
                global_offset_ += buffer_offset_;
                buffer_offset_ = 0;
                break;
            }
        }

    }
}

/*
 *undo phase on TABLE PAGE level(table/table_page.h)
 *iterate through active txn map and undo each operation
 */
void LogRecovery::Undo() {
    
    
    int num_record=active_txn_.size();
    char tmp_buffer[LOG_BUFFER_SIZE];
    for(auto it=active_txn_.begin();it!=active_txn_.end();it++){
    
    
        lsn_t lsn=it->second;
        while(true){
    
    
            int global_offset=lsn_mapping_[lsn].first;
            int log_size=lsn_mapping_[lsn].second;
            TablePage* page;
            LogRecord log;
            disk_manager_->ReadLog(log_buffer_,log_size,global_offset);
            assert(DeserializeLogRecord(log_buffer_,log));
            if(log.log_record_type_==LogRecordType::BEGIN){
    
    
                break;
            }

            if(log.log_record_type_==LogRecordType::INSERT){
    
    
                auto rid=log.insert_rid_;
                page = static_cast<TablePage *>(
                        buffer_pool_manager_->FetchPage(rid.GetPageId()));
                page->ApplyDelete(rid, nullptr, nullptr);
            }else if(log.log_record_type_==LogRecordType::APPLYDELETE){
    
    
                auto rid=RID();
                page = static_cast<TablePage *>(
                        buffer_pool_manager_->FetchPage(rid.GetPageId()));
                page->InsertTuple(log.delete_tuple_,rid, nullptr, nullptr, nullptr);
            }else if(log.log_record_type_==LogRecordType::MARKDELETE){
    
    
                auto rid=log.delete_rid_;
                page = static_cast<TablePage *>(
                        buffer_pool_manager_->FetchPage(rid.GetPageId()));
                page->RollbackDelete(rid, nullptr, nullptr);
            }else if(log.log_record_type_==LogRecordType::ROLLBACKDELETE){
    
    
                auto rid=log.delete_rid_;
                page = static_cast<TablePage *>(
                        buffer_pool_manager_->FetchPage(rid.GetPageId()));
                page->MarkDelete(rid, nullptr, nullptr, nullptr);
            }else if(log.log_record_type_==LogRecordType::UPDATE) {
    
    
                auto rid=log.update_rid_;
                page = static_cast<TablePage *>(
                        buffer_pool_manager_->FetchPage(rid.GetPageId()));
                page->UpdateTuple(log.old_tuple_,
                                  log.new_tuple_, rid,nullptr, nullptr, nullptr);
            }else if(log.log_record_type_==LogRecordType::NEWPAGE){
    
    
                page = static_cast<TablePage *>(
                        buffer_pool_manager_->FetchPage(log.cur_page_id));
                buffer_pool_manager_->DeletePage(log.cur_page_id);
                if(log.prev_page_id_!=INVALID_PAGE_ID){
    
    
                    auto pre_page=static_cast<TablePage *>(
                            buffer_pool_manager_->FetchPage(log.prev_page_id_));
                    pre_page->SetNextPageId(INVALID_PAGE_ID);
                    buffer_pool_manager_->UnpinPage(log.prev_page_id_,true);
                }
            }
            if(log.log_record_type_!=LogRecordType::BEGIN &&
               log.log_record_type_!=LogRecordType::COMMIT &&
               log.log_record_type_!=LogRecordType::ABORT){
    
    
                buffer_pool_manager_->UnpinPage(page->GetPageId(), true);
            }
            lsn=log.GetPrevLSN();
        }


    }
}

其中DeserializeLogRecord负责从log_buffer的指定位置读取一个log_record。首先看是否能读取一个header, 如果可以就利用header中保存的size加上当前的偏移量,如果大于log_buffer_size说明这个log_buffer中已经不能读取一个完整的buffer。
redo每次从log文件中读取log_buffer_size长度的数据到log_buffer,然后利用DeserializeLogRecord从log_buffer中读取log。 如果DeserializeLogRecord返回false,则从当前的log文件偏移量开始,又读取log_buffer_size长度的数据到log_buffer。如果当前的log_buffer中读取不到任何log, redo过程停止。redo过程中负责记录所有active table,以及表中所有事务对应的LSN最大的一条record, 同时构建lsn到record在log文件中的偏移地址以及record长度的映射。

undo 负责根据active table 分别对表中的事务进行回滚,利用pre_lsn找到当前record所属事务中的上一条record。 redo 和undo中的所有操作(insert,update…) 都是直接在record指向的table_page上进行, 而不是利用table_heap执行。

table_heap中在执行某个操作之前,会先获取这个操作涉及page的写锁,令人困惑的地方是当table_heap把操作权转交给table_page时,table_page内部又会利用lock_manager获取rid的写锁, 这会给人一种重复加锁的错觉。实际上,page上的锁在操作完成后会马上释放,而rid上的锁会一直保持到事务commit或abort。insert操作中,虽然对于当前事务来说, rid是全新的,但是这个rid可能是其它事务进行applyDelete操作后留下的空位,所以可能造成rid锁的争用。table_page在insert操作中如果争用rid锁失败(可能因为deadlock prevention 协议),需要返回false,并把当前事务的状态设置为aborted。 不能指望利用abort操作来回滚因获取锁而失败的insert操作,因为回滚时的delete操作同样也获取不到锁,所以只能在返回false之前手动取消insert。

  • 测试
    在debug模式下通过sqlite3 加载动态库的方式运行内核

    rc = sqlite3_open(db_file.c_str(), &db);
      EXPECT_EQ(rc, SQLITE_OK);
    
      rc = sqlite3_enable_load_extension(db, 1);
      EXPECT_EQ(rc, SQLITE_OK);
    
      const char *zFile = "../lib/libvtable.so"; // shared library name
      const char *zProc = 0;           // entry point within library
      char *zErrMsg = 0;
      rc = sqlite3_load_extension(db, zFile, zProc, &zErrMsg);
      EXPECT_EQ(rc, SQLITE_OK);
    
      EXPECT_TRUE(ExecSQL(
          db, "CREATE VIRTUAL TABLE foo1 USING vtable ('a INT, b "
              "int, c smallint, d varchar, e bigint, f bool', 'foo1_pk b')"));
    
      EXPECT_TRUE(ExecSQL(db, "INSERT INTO foo1 VALUES(1, 2, 3, 'hello', 2,1)"));
      EXPECT_TRUE(ExecSQL(db, "INSERT INTO foo1 VALUES(3, 4, 5, 'Nihao',4, 1)"));
      EXPECT_TRUE(ExecSQL(db, "INSERT INTO foo1 VALUES(2, 3, 4, 'world',3, 1)"));
      EXPECT_TRUE(ExecSQL(db, "SELECT * FROM foo1"));
      EXPECT_TRUE(ExecSQL(db, "DELETE FROM foo1 WHERE b = 2"));
      EXPECT_TRUE(ExecSQL(db, "SELECT * FROM foo1"));
      EXPECT_TRUE(ExecSQL(db, "DROP TABLE foo1"));
    

猜你喜欢

转载自blog.csdn.net/weixin_39849839/article/details/116675938
CMU
今日推荐