并发索引

本文探讨了在多线程环境中,如何实现并发的Latch读写模式,特别是在数据库索引管理中的应用。详细介绍了Reader-Writer Locks的实现,包括数据成员、构造析构函数以及读写锁的获取与释放。同时,提到了蟹行协议在并发Search、Insert操作中的应用,确保了并发操作的安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Latch的读写模式
  • 读模式:多个线程在同一时间能够读取相同的对象;如果一个线程获持有一个处于读模式的latch,另一个线程也能够获取该处于读模式的latch。
  • 写模式:只有一个线程能够获取该对象;如果有其他线程持有处于任意模式下的latch,则其他线程是不能够获得write latch的。
  • 相容性矩阵:
    latch读写模式的相容性矩阵

Latch的实现

Reader-Writer Locks
  • 允许多个并发读者
  • 必须管理读写队列来避免饿死
  • 可以在自旋锁的基础上加以实现

可以用互斥量和条件变量来实现rwlatch, 在src/include/common/rwlatch.h 中实现了 ReaderWriterLatch 类。

数据成员和构造析构函数

互斥量 mutex_ 来保护当前的读者个数reader_count_ (读者队列上最多有 UINT_MAX 个读者)和是否有写者进入writer_entered_。当可以获取wlatch或rlatch时,用条件变量 writer_reader_唤醒其他读写线程。需要注意的是析构函数在析构 mutex_ 变量的时候,先使用了 guard 进行保护;其次是成员变量的声明顺序。

using mutex_t = std::mutex;
using cond_t = std::condition_variable;
static const uint32_t MAX_READERS = UINT_MAX;
public:
  ReaderWriterLatch() = default;
  ~ReaderWriterLatch() { std::lock_guard<mutex_t> guard(mutex_); }
private:
 mutex_t mutex_;
 cond_t writer_;
 cond_t reader_;
 uint32_t reader_count_{0};
 bool writer_entered_{false};
 
获取读锁和释放读锁

获取读锁的时候,在第 6 行代码可以看到,当有写者进入或读者数量达到最大值时,新进入的读者会睡眠在条件变量 reader_ 上;释放读锁的时候在 18~21 行首先检查是否有写者进入,如果释放后当前读者数量为0,则通知其中一个写线程,然后在 22~25 行检查当前读者数量,很明显当有读者持有该读锁时,其他读者也可以轻易获取该读锁,当释放该读锁后当前读者数量等于 MAX_READERS - 1 时,才可能会有其他读线程等待,此时需要唤醒它们。

01  /**
02   * Acquire a read latch.
03   */
04  void RLock() {
05    std::unique_lock<mutex_t> latch(mutex_);
06    while (writer_entered_ || reader_count_ == MAX_READERS) {
07      reader_.wait(latch);
08    }
09    reader_count_++;
10  }
11
12  /**
13   * Release a read latch.
14   */
15  void RUnlock() {
16    std::lock_guard<mutex_t> guard(mutex_);
17    reader_count_--;
18    if (writer_entered_) {
19      if (reader_count_ == 0) {
20        writer_.notify_one();
21      }
22    } else {
23      if (reader_count_ == MAX_READERS - 1) {
24        reader_.notify_one();
25      }
26    }
27  }
获取写锁和释放写锁

在第 6~8 行可以看到获取写锁时,如果当前已经有写者进入了,则其他读线程会睡眠在 reader_ 上,不能获得进行读,即使当前没有写线程获取该锁,在 9~12 行可以看到,当读者数量不为0时,进入的写者也不能进行写操作,会睡眠在 writer_ 条件变量上。释放写锁时需要注意第 21 行会通知所有的读写线程。

01  /**
02   * Acquire a write latch.
03   */
04  void WLock() {
05    std::unique_lock<mutex_t> latch(mutex_);
06    while (writer_entered_) {
07      reader_.wait(latch);
08    }
09    writer_entered_ = true;
10    while (reader_count_ > 0) {
11      writer_.wait(latch);
12    }
13  }
14
15  /**
16   * Release a write latch.
17   */
18  void WUnlock() {
19    std::lock_guard<mutex_t> guard(mutex_);
20    writer_entered_ = false;
21    reader_.notify_all();
22  }
页面上锁

在缓冲池管理器中 FetchPage() 的返回值是一个指向 Page 实例的指针(src/include/storage/ Page/Page.h)。可以获取 Page 上的 latch ,但不能获取B+Tree 节点上的 latch (内部节点和叶节点都不行) 。src/include/storage/page/page.h 增加了在页面上获取和释放latch的辅助函数。

  /** 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(); }
  /** Page latch. */
 ReaderWriterLatch rwlatch_;
Blocking OS Mutex
Test-and-Set Spinlock

蟹行协议(crabbing protocol)

我们需要更新原始的单线程 B+Tree 索引,以便它能够支持并发操作。我们将使用课堂上和课本上描述的蟹行协议进行封锁。遍历索引的线程将获取然后释放 B+Tree 页面上的 latch。一个线程只能在它的子页面被认为是“安全的”时释放父页面上的 latch。注意,“安全”的定义可能会根据线程正在执行的操作的类型而变化。

  • Search: 从根页面开始,获取子页面上的read ® latch,然后在到达子页面时释放父页面上的latch
  • Insert: :从根页面开始,获取子页面上的write (W) latch。一旦child被锁定,检查它是否安全,在这种情况下,如果是不满的。如果child是安全的,释放所有祖先的锁。
  • Delete: 从根页面开始,获取子页面上的write (W) latch。一旦child被锁定,检查它是否安全,在这种情况下,至少是半满。(注意:对于根页面,我们需要用不同的标准进行检查)如果child是安全的,释放所有祖先的锁。
Search(GetValue)的并发:

先看下单线程版本的GetValue 操作:

  1. 查找叶子页面
  2. 在叶子页面里面查找key
  3. unpin
  4. 返回结果
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::GetValue(const KeyType &key, std::vector<ValueType> *result, Transaction *transaction) {

  Page* leaf_page = FindLeafPage(key, false);
  LeafPage *leaf = reinterpret_cast<LeafPage *>(leaf_page->GetData());
  
  ValueType value;
  bool ret = leaf->Lookup(key, &value, comparator_);
  if (ret) {
    result->emplace_back(std::move(value));
  }
  buffer_pool_manager_->UnpinPage(leaf->GetPageId(), false);
  
  return ret;
}

着重需要考虑的是叶子页面的查找

INDEX_TEMPLATE_ARGUMENTS
Page *BPLUSTREE_TYPE::FindLeafPage(const KeyType &key, bool leftMost) {
  if (IsEmpty()) {
    return nullptr;
  }

  Page *page = buffer_pool_manager_->FetchPage(root_page_id_);
  BPlusTreePage *root = reinterpret_cast<BPlusTreePage *>(page->GetData());
  while (!root->IsLeafPage()) {
    InternalPage *inner = reinterpret_cast<InternalPage *>(root);
    auto page_id = leftMost ? inner->ValueAt(0) : inner->Lookup(key, comparator_);
    buffer_pool_manager_->UnpinPage(inner->GetPageId(), false);
    page = buffer_pool_manager_->FetchPage(page_id);
    root = reinterpret_cast<BPlusTreePage *>(page->GetData());
  }

  return page;
}

Insert 的并发

先看下单线程版本的Insert 操作

INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::Insert(const KeyType &key, const ValueType &value, Transaction *transaction) {
  if (IsEmpty()) {
    StartNewTree(key, value);
    return true;
  }

  return InsertIntoLeaf(key, value, transaction);
}
INDEX_TEMPLATE_ARGUMENTS
bool BPLUSTREE_TYPE::InsertIntoLeaf(const KeyType &key, const ValueType &value, Transaction *transaction) {
  // find the right leaf page as insertion target
  page_id_t leaf_page_id = FindLeafPage(key, transaction, Operation::SEARCH);
  Page *leaf_page = buffer_pool_manager_->FetchPage(leaf_page_id);
  leaf_page->WLatch();
  UnlockUnpinAncestor(transaction, Operation::SEARCH);
  transaction->AddIntoPageSet(leaf_page);
  LeafPage *leaf = reinterpret_cast<LeafPage *>(leaf_page->GetData());

  //  then look through leaf page to see whether insert key exist or not. If exist, return immdiately
  ValueType useless_value;
  if (leaf->Lookup(key, &useless_value, comparator_)) {
    UnlockUnpinAncestor(transaction, Operation::INSERT);
    return false;
  }
  // safe page
  if (IsSafePage(leaf_page, Operation::INSERT)) {
    // otherwise insert entry.
    leaf->Insert(key, value, comparator_);
    UnlockUnpinAncestor(transaction, Operation::INSERT);
    return true;
  }

  // If the leaf is not safe, release all previous latches,
  // and restart the transaction using previous Insert/Delete protocol
  UnlockUnpinAncestor(transaction, Operation::INSERT);

  root_mutex_.WLock();
  if (IsEmpty()) {
    StartNewTree(key, value);
    root_mutex_.WUnlock();
    //UnlockUnpinAncestor(transaction, Operation::INSERT);
    return true;
  }

  leaf_page_id = FindLeafPage(key, transaction, Operation::INSERT);
  leaf_page = buffer_pool_manager_->FetchPage(leaf_page_id);
  leaf_page->WLatch();
  transaction->AddIntoPageSet(leaf_page);
  leaf = reinterpret_cast<LeafPage *>(leaf_page->GetData());

  //ValueType useless_value;
  if (leaf->Lookup(key, &useless_value, comparator_)) {
    // buffer_pool_manager_->UnpinPage(leaf->GetPageId(), false);
    UnlockUnpinAncestor(transaction, Operation::INSERT);
    return false;
  }


  leaf->Insert(key, value, comparator_);

  // Remember to deal with split if necessary.
  if (leaf->GetSize() == leaf->GetMaxSize()) {
    KeyType mid_key = leaf->KeyAt(leaf->GetSize() / 2);
    BPlusTreePage *old_node = reinterpret_cast<BPlusTreePage *>(leaf);
    BPlusTreePage *new_node = Split(old_node);
    InsertIntoParent(old_node, mid_key, new_node, transaction);
    buffer_pool_manager_->UnpinPage(new_node->GetPageId(), true);
  }
  // buffer_pool_manager_->UnpinPage(leaf->GetPageId(), true);
  UnlockUnpinAncestor(transaction, Operation::INSERT);
  return true;
}

注意:

  1. 根节点上的Lock和UnLock的时机,防止死锁
  2. 性能调优
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值