[已完結]CMU資料庫(15-445)實驗2-B+樹索引實現(下)

周小倫發表於2021-01-27

4. Index_Iterator實現

這裡就是需要實現迭代器的一些操作,比如begin、end、isend等等

下面是對於IndexIterator的建構函式

template <typename KeyType, typename ValueType, typename KeyComparator>
IndexIterator<KeyType, ValueType, KeyComparator>::
IndexIterator(BPlusTreeLeafPage<KeyType, ValueType, KeyComparator> *leaf,
              int index_, BufferPoolManager *buff_pool_manager):
    leaf_(leaf), index_(index_), buff_pool_manager_(buff_pool_manager) {}

1. 首先我們來看begin函式的實現

  1. 利用key值找到葉子結點
  2. 然後獲取當前key值的index就是begin的位置
INDEX_TEMPLATE_ARGUMENTS
INDEXITERATOR_TYPE BPLUSTREE_TYPE::Begin(const KeyType &key) {
  auto leaf = reinterpret_cast<BPlusTreeLeafPage<KeyType, ValueType,KeyComparator> *>(FindLeafPage(key, false));
  int index = 0;
  if (leaf != nullptr) {
    index = leaf->KeyIndex(key, comparator_);
  }
  return IndexIterator<KeyType, ValueType, KeyComparator>(leaf, index, buffer_pool_manager_);
}

2. end函式的實現

  1. 找到最開始的結點
  2. 然後一直向後遍歷直到nextPageId=-1結束
  3. 這裡注意需要過載!===

end函式

INDEX_TEMPLATE_ARGUMENTS
INDEXITERATOR_TYPE BPLUSTREE_TYPE::end() {
  KeyType key{};
  auto leaf= reinterpret_cast<BPlusTreeLeafPage<KeyType, ValueType,KeyComparator> *>( FindLeafPage(key, true));
  page_id_t new_page;
  while(leaf->GetNextPageId()!=INVALID_PAGE_ID){
    new_page=leaf->GetNextPageId();
    leaf=reinterpret_cast<BPlusTreeLeafPage<KeyType, ValueType,KeyComparator> *>(buffer_pool_manager_->FetchPage(new_page));
  }
  buffer_pool_manager_->UnpinPage(new_page,false);
  return IndexIterator<KeyType, ValueType, KeyComparator>(leaf, leaf->GetSize(), buffer_pool_manager_);
}

==和 !=函式

bool operator==(const IndexIterator &itr) const {
  return this->index_==itr.index_&&this->leaf_==itr.leaf_;
}

bool operator!=(const IndexIterator &itr) const {
  return !this->operator==(itr);
}

3. 過載++和*(解引用符號)

  1. 過載++

簡單的index++然後設定nextPageId即可

template <typename KeyType, typename ValueType, typename KeyComparator>
IndexIterator<KeyType, ValueType, KeyComparator> &IndexIterator<KeyType, ValueType, KeyComparator>::
operator++() {
//
 // std::cout<<"++"<<std::endl;
  ++index_;
  if (index_ == leaf_->GetSize() && leaf_->GetNextPageId() != INVALID_PAGE_ID) {
    // first unpin leaf_, then get the next leaf
    page_id_t next_page_id = leaf_->GetNextPageId();

    auto *page = buff_pool_manager_->FetchPage(next_page_id);
    if (page == nullptr) {
      throw Exception("all page are pinned while IndexIterator(operator++)");
    }
    // first acquire next page, then release previous page
    page->RLatch();

    buff_pool_manager_->FetchPage(leaf_->GetPageId())->RUnlatch();
    buff_pool_manager_->UnpinPage(leaf_->GetPageId(), false);
    buff_pool_manager_->UnpinPage(leaf_->GetPageId(), false);

    auto next_leaf =reinterpret_cast<BPlusTreeLeafPage<KeyType, ValueType,KeyComparator> *>(page->GetData());
    assert(next_leaf->IsLeafPage());
    index_ = 0;
    leaf_ = next_leaf;
  }
  return *this;
};
  1. 過載*

return array[index]即可

template <typename KeyType, typename ValueType, typename KeyComparator>
const MappingType &IndexIterator<KeyType, ValueType, KeyComparator>::
operator*() {
  if (isEnd()) {
    throw "IndexIterator: out of range";
  }
  return leaf_->GetItem(index_);
}

5. 併發機制的實現

0. 首先複習一下讀寫?機制

  1. 讀操作是可以多個程式之間共享latch的而寫操作則必須互斥
  2. 加入MaxReader數就是為了防止等待的⌛️寫程式飢餓

首先來看如果沒有?機制多執行緒會發生什麼問題

  1. 執行緒T1想要刪除44。
  2. 執行緒T2 想要查詢41
image-20210126184533688
  1. 假設T2在執行到D位置的時候又切換到執行緒T1
  2. 這個時候T1進行重新分配,會把41借到I結點上
  3. T1執行完成切換回T2這時候T2再去原來的執行尋找41就會找不到
image-20210126184727498

就會出現下面的情況。❓

image-20210126184901306

由此我們需要讀寫?的存在

  1. 對於find操作

由於我們是隻讀操作,所以我們到下一個結點的時候就可以釋放上一個結點的Latch

image-20210126185917549

剩下的操作都是一樣的

  1. 對於delete則不一樣

因為我們需要寫操作

這裡我們不能釋放結點A的Latch。因為我們的刪除操作可能會合並根節點。

image-20210126190112632

到D的時候。我們會發現D中的38刪除之後不需要進行合併,所以對於A和B的寫Write是可以安全釋放了

image-20210126190229333
  1. 對於Insert操作

這裡我們就可以安全的釋放掉A的鎖。因為B中還有空位,我們插入是不會對A造成影響的

image-20210126190452937

當我們執行到D這裡發現D中已經滿了。所以此時我們不會釋放B的鎖,因為我們會對B進行寫操作

image-20210126190613339

上面的演算法雖然是正確的但是有瓶頸問題。由於只有一個執行緒可以獲得寫Latch。而插入和刪除的時候都需要對頭結點加寫Latch。所以多執行緒在有許多個插入或者刪除操作的時候,效能就會大打折扣

這裡要引入樂觀?

樂觀的假設大部分操作是不需要進行合併和分裂的。因此在我們向下的時候都是讀Latch而不是寫Latch。只有在葉子結點才是write Latch

  1. 從上到下都是讀Latch。而且逐步釋放
  2. 到葉子結點需要修改的時候才為寫Latch。這個刪除是安全的所以直接結束
image-20210126192408392
  1. 當我們到最後一步發現不安全的時候。則需要像上面我們沒有引入樂觀?的時候一樣。重新執行一遍
image-20210126192548748

B-Link Tree簡介

延遲更新父結點

這裡用一個?來標記這裡需要被更新但是還沒有執行

image-20210126195848104

這個時候我們執行其他操作也是正確的比如查詢31

image-20210126200003320

這裡我們執行insert 33

當執行到結點C的時候。因為這個時候有另一個執行緒持有了write Latch。所以這個時候?操作要執行。隨後在插入33

[已完結]CMU資料庫(15-445)實驗2-B+樹索引實現(下)

最後一點補充關於掃描操作的

  1. 執行緒1在C結點上持有write Latch
  2. 執行緒2已經掃描完了結點B想要獲得結點C的read Latch

這時候會發生問題,因為執行緒2無法拿到read Latch

這裡有幾種解決方法

  1. 可以等到T1的寫操作完成
  2. 可以重新執行T2
  3. 可以直接讓執行緒T2停止搶得這個Latch。
[已完結]CMU資料庫(15-445)實驗2-B+樹索引實現(下)

注意這裡的LatchLock並不一樣

img

1. 輔助函式UnlockUnpinPages的實現

  1. 如果是讀操作則釋放read鎖
  2. 否則釋放write鎖
INDEX_TEMPLATE_ARGUMENTS
void BPLUSTREE_TYPE::
UnlockUnpinPages(Operation op, Transaction *transaction) {
  if (transaction == nullptr) {
    return;
  }

  for (auto page:*transaction->GetPageSet()) {
    if (op == Operation::READ) {
      page->RUnlatch();
      buffer_pool_manager_->UnpinPage(page->GetPageId(), false);
    } else {
      page->WUnlatch();
      buffer_pool_manager_->UnpinPage(page->GetPageId(), true);
    }
  }
  transaction->GetPageSet()->clear();

  for (const auto &page_id: *transaction->GetDeletedPageSet()) {
    buffer_pool_manager_->DeletePage(page_id);
  }
  transaction->GetDeletedPageSet()->clear();

  // if root is locked, unlock it

  node_mutex_.unlock();
  }

四個自帶的解鎖和上鎖操作

/** Acquire the page write latch. */
inline void WLatch() { rwlatch_.WLock(); }

/** Release the page write latch. */
inline void WUnlatch() { rwlatch_.WUnlock(); }

/** Acquire the page read latch. */
inline void RLatch() { rwlatch_.RLock(); }

/** Release the page read latch. */
inline void RUnlatch() { rwlatch_.RUnlock(); }

這裡的rwlatch是自己實現的讀寫鎖類下面來探究一下這個類

由於c++ 併發程式設計我現在還不太會。。。所以就簡單看一下啦後面學完併發程式設計再補充

  1. WLock函式

    1. 首先獲取一個鎖
    2. 用一個記號writer_entered表示是否有寫操作
    3. 如果之前已經有了現在的操作就需要等(這個執行緒處於阻塞狀態)
    4. 當前如果有其他執行緒執行讀操作。則仍需要阻塞(別人讀的時候你不能寫)
    void WLock() {
      std::unique_lock<mutex_t> latch(mutex_);
      while (writer_entered_) {
        reader_.wait(latch);
      }
      writer_entered_ = true;
      while (reader_count_ > 0) {
        writer_.wait(latch);
      }
    }
    
  2. WunLock函式

    1. 寫標記置為false
    2. 然後通知所有的執行緒
    void WUnlock() {
      std::lock_guard<mutex_t> guard(mutex_);
      writer_entered_ = false;
      reader_.notify_all();
    }
    
  3. RLock函式

    1. 如果當前有人在寫或者已經有最多的人讀了則阻塞
    2. 否則只需要讓讀的計數++

    因為是允許多個執行緒一起讀這樣並不會出錯

    void RLock() {
      std::unique_lock<mutex_t> latch(mutex_);
      while (writer_entered_ || reader_count_ == MAX_READERS) {
        reader_.wait(latch);
      }
      reader_count_++;
    }
    
  4. RUnLatch函式

    1. 計數--
    2. 如果當前有人在寫並且無人讀的話需要通知所有其他執行緒
    3. 如果在計數--之前達到了最大讀數,釋放這個鎖之後需要通知其他執行緒,現在又可以讀了。
    void RUnlock() {
      std::lock_guard<mutex_t> guard(mutex_);
      reader_count_--;
      if (writer_entered_) {
        if (reader_count_ == 0) {
          writer_.notify_one();
        }
      } else {
        if (reader_count_ == MAX_READERS - 1) {
          reader_.notify_one();
        }
      }
    }
    

6. Summary

好了終於磕磕絆絆的寫完了Lab2.關於資料庫的Lab2應該會停一段時間。這段時間要補一補深度學習(畢竟要畢業)然後趕工一下老師給的活。同時學一下c++併發程式設計和看一下侯捷老師的課程。

最後附上GitHub的?
https://github.com/JayL-zxl/CMU15-445Lab

相關文章