CMU15445 (Fall 2020) 之 Project#1 - Buffer Pool 详解

这篇具有很好参考价值的文章主要介绍了CMU15445 (Fall 2020) 之 Project#1 - Buffer Pool 详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

去年暑假完成了 CMU15-445 Fall 2019 的四个实验,分别对应下述博客:

  • CMU15445 (Fall 2019) 之 Project#1 - Buffer Pool 详解
  • CMU15445 (Fall 2019) 之 Project#2 - Hash Table 详解
  • CMU15445 (Fall 2019) 之 Project#3 - Query Execution 详解
  • CMU15445 (Fall 2019) 之 Project#4 - Logging & Recovery 详解

今年打算接着完成 Fall 2020 的四个实验,同时解读一下课程组写好的那一部分代码,比如数据存储和页面布局的代码,加深自己对数据库系统的理解。

环境搭建

在 GitHub 上新建一个私有仓库,命名为 CMU15445-Fall2020,然后将官方仓库克隆到本地:

git clone git@github.com:cmu-db/bustub.git ./cmu15445-fall2020
cd cmu15445-fall2020

目前官方的代码应该更新到 Fall2023 了,需要回滚到 Fall2020,并将代码传到自己的远程仓库:

git reset --hard 444765a

git remote rm origin
git remote add origin git@github.com:zhiyiYo/cmu15445-fall2020.git #添加自己仓库作为远程分支
git push -u origin main

实验环境为 Ubuntu20.04 虚拟机,所以执行下述代码安装依赖包:

sudo build_support/packages.sh

和去年一样,因为 googletest 仓库将 master 分支重命名为 main 了,所以需要将 build_support/gtest_CMakeLists.txt.in 的内容改为:

cmake_minimum_required(VERSION 3.8)

project(googletest-download NONE)

include(ExternalProject)
ExternalProject_Add(googletest
        GIT_REPOSITORY git@github.com:google/googletest.git
        GIT_TAG main
        SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src"
        BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build"
        CONFIGURE_COMMAND ""
        BUILD_COMMAND ""
        INSTALL_COMMAND ""
        TEST_COMMAND ""
        )

最后编译一下,如果编译成功就说明环境搭建完成:

mkdir build
cd build
cmake ..
make

缓存池

由于磁盘读写速度远慢于内存,所以数据库会在内存中开辟一块连续空间,用于存储最近访问的页,这块空间称为缓存池。执行引擎不会直接从磁盘读取页,而是向缓存池要。如果缓存池中没有想要的页,就会从磁盘读入到池中,然后返回给执行引擎。页内数据更新后也不会立即写入磁盘,而是打上了一个 Dirty 标志位并暂存在缓存池中,等到时机成熟再写入。

CMU15445 (Fall 2020) 之 Project#1 - Buffer Pool 详解

缓冲池的本质是一个数组,只能存一定数量的页。如果执行引擎想要的 Page 不在缓存池中,且缓存池已满,这时候需要从中踢出一个页来腾出空间给新 Page,被踢出的 Dirty 页需要被保存到磁盘中来保证数据一致性。需要指出的是,不是任何 Page 都能被换出,那些正在被使用的页不能换出,而判断一个页是否正被使用的依据是 Page 内部保存的 Pin/Reference 计数器,只要计数器的值大于 0,就说明至少有一个线程在使用它。

缓冲池内部维护着一个 page_idframe_id 的映射表,用来指出页和内部数组索引的映射关系。同时内部还有一个互斥锁来保证并发安全,对缓存池的增删改查都需要上锁。

CMU15445 (Fall 2020) 之 Project#1 - Buffer Pool 详解

实验要求

任务 1:LRU Replacement Policy

Fall2019 要求实现的是时钟替换算法,而 Fall2020 则改成了 LRU 替换算法,实现方式一般使用双向链表 + 哈希表,C艹 可以直接用标准库中的 std::liststd::unordered_map。双向链表中存放允许被换出的 frame_id,哈希表中存 frame_id 及其对应的双向链表迭代器,这样可以实现 \(O(1)\) 复杂度的读写。链表的表头处存放最近访问的 frame_id,而尾处则是距离上次访问时间最远的的 frame_id

class LRUReplacer : public Replacer {
 public:
  /**
   * Create a new LRUReplacer.
   * @param num_pages the maximum number of pages the LRUReplacer will be required to store
   */
  explicit LRUReplacer(size_t num_pages);

  ~LRUReplacer() override;

  /**
   * Remove the victim frame as defined by the replacement policy.
   * @param[out] frame_id id of frame that was removed, nullptr if no victim was found
   * @return true if a victim frame was found, false otherwise
   */
  bool Victim(frame_id_t *frame_id) override;

  /**
   * Pins a frame, indicating that it should not be victimized until it is unpinned.
   * @param frame_id the id of the frame to pin
   */
  void Pin(frame_id_t frame_id) override;

  /**
   * Unpins a frame, indicating that it can now be victimized.
   * @param frame_id the id of the frame to unpin
   */
  void Unpin(frame_id_t frame_id) override;

  /** @return the number of elements in the replacer that can be victimized */
  size_t Size() override;

 private:
  size_t num_pages_;
  std::list<frame_id_t> list_;
  std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> map_;
  std::shared_mutex mutex_;
};

具体实现如下所示,可以看到 LRUReplacer 对缓冲池中存了多少页以及存了哪些页是一无所知的,它只关心能被换出的 frame_id,外界通过调用 LURReplacer::Unpin() 添加一个能被换出的 frame_id,调用 LRUReplacer::Pin() 来移除一个 frame_id

LRUReplacer::LRUReplacer(size_t num_pages) : num_pages_(num_pages) {}

LRUReplacer::~LRUReplacer() = default;

bool LRUReplacer::Victim(frame_id_t *frame_id) {
  lock_guard<shared_mutex> lock(mutex_);

  if (Size() == 0) {
    return false;
  }

  *frame_id = list_.back();
  list_.pop_back();
  map_.erase(*frame_id);

  return true;
}

void LRUReplacer::Pin(frame_id_t frame_id) {
  lock_guard<shared_mutex> lock(mutex_);

  // frame 需要在缓冲池中
  if (!map_.count(frame_id)) {
    return;
  }

  auto it = map_[frame_id];
  map_.erase(frame_id);
  list_.erase(it);
}

void LRUReplacer::Unpin(frame_id_t frame_id) {
  lock_guard<shared_mutex> lock(mutex_);

  // 缓冲池满了不能插入新的 page,不能重复插入 page
  if (Size() == num_pages_ || map_.count(frame_id)) {
    return;
  }

  list_.push_front(frame_id);
  map_[frame_id] = list_.begin();
}

size_t LRUReplacer::Size() {
  return list_.size();
}

在终端输入命令:

mkdir build
cd build
cmake ..
make lru_replacer_test
./test/lru_replacer_test

测试结果如下:

CMU15445 (Fall 2020) 之 Project#1 - Buffer Pool 详解

任务2:Buffer Pool Manager

BufferPoolManager 用于管理缓冲池,内部有一个 DiskManager 来读写磁盘数据,LRUReplacer 执行替换算法。这个类要求我们实现五个函数:

  • FetchPageImpl(page_id)
  • NewPageImpl(page_id)
  • UnpinPageImpl(page_id, is_dirty)
  • FlushPageImpl(page_id)
  • DeletePageImpl(page_id)
  • FlushAllPagesImpl()

下面会一个个实现上述函数。

FetchPageImpl(page_id)

该函数实现了缓冲池的主要功能:向上层提供指定的 page。缓冲池管理器首先在 page_table_ 中查找 page_id 键是否存在:

  • 如果存在就根据 page_id 对应的 frame_id 从缓冲池 pages_ 取出 page
  • 如果不存在就通过 GetVictimFrameId() 函数选择被换出的帧,该函数首先从 free_list_ 中查找缓冲池的空位,如果没找到空位就得靠上一节实现的 LRUReplacer 选出被换出的冤大头

具体代码如下:


Page *BufferPoolManager::FetchPageImpl(page_id_t page_id) {
  lock_guard<mutex> lock(latch_);

  // 1.     Search the page table for the requested page (P).
  Page *page;
  auto it = page_table_.find(page_id);

  // 1.1    If P exists, pin it and return it immediately.
  if (it != page_table_.end()) {
    auto frame_id = it->second;
    page = &pages_[frame_id];
    replacer_->Pin(frame_id);
    page->pin_count_++;
    return page;
  }

  // 1.2    If P does not exist, find a replacement page (R) from either the free list or the replacer.
  //        Note that pages are always found from the free list first.
  auto frame_id = GetVictimFrameId();
  if (frame_id == INVALID_PAGE_ID) {
    return nullptr;
  }

  // 2.     If R is dirty, write it back to the disk.
  page = &pages_[frame_id];
  if (page->IsDirty()) {
    disk_manager_->WritePage(page->page_id_, page->data_);
  }

  // 3.     Delete R from the page table and insert P.
  page_table_.erase(page->page_id_);
  page_table_[page_id] = frame_id;

  // 4.     Update P's metadata, read in the page content from disk, and then return a pointer to P.
  disk_manager_->ReadPage(page_id, page->data_);
  page->update(page_id, 1, false);
  replacer_->Pin(frame_id);

  return page;
}

frame_id_t BufferPoolManager::GetVictimFrameId() {
  frame_id_t frame_id = INVALID_PAGE_ID;

  if (!free_list_.empty()) {
    frame_id = free_list_.front();
    free_list_.pop_front();
  } else {
    replacer_->Victim(&frame_id);
  }

  return frame_id;
}

上述代码中还用了一个 Page::update 辅助函数,用于更新 page 的元数据:

/**
* update the meta data of page
* @param page_id the page id
* @param pin_count the pin count
* @param is_dirty is page dirty
* @param reset_memory whether to reset the memory of page
*/
void update(page_id_t page_id, int pin_count, bool is_dirty, bool reset_memory = false) {
  page_id_ = page_id;
  pin_count_ = pin_count;
  is_dirty_ = is_dirty;
  if (reset_memory) {
    ResetMemory();
  }
}

NewPageImpl(page_id)

该函数在缓冲池中插入一个新页,如果缓冲池中的所有页面都正在被线程访问,插入失败,否则靠 GetVictimFrameId() 计算插入位置:


Page *BufferPoolManager::NewPageImpl(page_id_t *page_id) {
  // 0.   Make sure you call DiskManager::AllocatePage!
  lock_guard<mutex> lock(latch_);

  // 1.   If all the pages in the buffer pool are pinned, return nullptr.
  auto frame_id = GetVictimFrameId();
  if (frame_id == INVALID_PAGE_ID) {
    return nullptr;
  }

  // 2.   Pick a victim page P from either the free list or the replacer. Always pick from the free list first.
  auto page = &pages_[frame_id];
  if (page->IsDirty()) {
    disk_manager_->WritePage(page->page_id_, page->data_);
  }

  // 3.   Update P's metadata, zero out memory and add P to the page table.
  *page_id = disk_manager_->AllocatePage();
  page_table_.erase(page->page_id_);
  page_table_[*page_id] = frame_id;
  page->update(*page_id, 1, false, true);
  replacer_->Pin(frame_id);

  // 4.   Set the page ID output parameter. Return a pointer to P.
  return page;
}

DeletePageImpl(page_id)

该函数从缓冲池和数据库文件中删除一个 page,并将其 page_id 设置为 INVALID_PAGE_ID

bool BufferPoolManager::DeletePageImpl(page_id_t page_id) {
  // 0.   Make sure you call DiskManager::DeallocatePage!
  lock_guard<mutex> lock(latch_);

  // 1.   Search the page table for the requested page (P).
  // 1.   If P does not exist, return true.
  auto it = page_table_.find(page_id);
  if (it == page_table_.end()) {
    return true;
  }

  // 2.   If P exists, but has a non-zero pin-count, return false. Someone is using the page.
  auto frame_id = it->second;
  auto &page = pages_[frame_id];
  if (page.pin_count_ > 0) {
    return false;
  }

  // 3.   Otherwise, P can be deleted. Remove P from the page table, reset its metadata and return it to the free list.
  disk_manager_->DeallocatePage(page_id);
  page_table_.erase(page.page_id_);
  free_list_.push_back(frame_id);
  page.update(INVALID_PAGE_ID, 0, false);

  return true;
}

UnpinPageImpl(page_id, is_dirty)

该函数用以减少对某个页的引用数 pin count,当 pin_count 为 0 时需要将其添加到 LRUReplacer 中:


bool BufferPoolManager::UnpinPageImpl(page_id_t page_id, bool is_dirty) {
  lock_guard<mutex> lock(latch_);

  auto it = page_table_.find(page_id);
  if (it == page_table_.end()) {
    return false;
  }

  auto frame_id = it->second;
  auto &page = pages_[frame_id];
  if (page.pin_count_ <= 0) {
    return false;
  }

  page.is_dirty_ |= is_dirty;
  if (--page.pin_count_ == 0) {
    replacer_->Unpin(frame_id);
  }
  return true;
}

FlushPageImpl(page_id)

该函数将缓冲池中的页写入磁盘以保持同步,这里不管页是否为脏,一律写入磁盘,不然并发的测试用例过不了:

bool BufferPoolManager::FlushPageImpl(page_id_t page_id) {
  // Make sure you call DiskManager::WritePage!
  lock_guard<mutex> lock(latch_);

  auto it = page_table_.find(page_id);
  if (it == page_table_.end()) {
    return false;
  }

  auto &page = pages_[it->second];
  disk_manager_->WritePage(page_id, page.data_);
  page.is_dirty_ = false;
  return true;
}

FlushAllPagesImpl()

该函数将缓冲池中的所有 page 写入磁盘:

void BufferPoolManager::FlushAllPagesImpl() {
  lock_guard<mutex> lock(latch_);

  for (auto &[page_id, frame_id] : page_table_) {
    auto &page = pages_[frame_id];
    if (page.IsDirty()) {
      disk_manager_->WritePage(page_id, page.data_);
      page.is_dirty_ = false;
    }
  }
}

测试

在终端输入指令:

cd build

make buffer_pool_manager_test
./test/buffer_pool_manager_test

# 下面是从 gradescope 扒下来的测试用例
make buffer_pool_manager_concurrency_test
./test/buffer_pool_manager_concurrency_test

测试结果如下:

CMU15445 (Fall 2020) 之 Project#1 - Buffer Pool 详解

总结

这个实验主要考察学生对并发和 STL 的掌握程度,由于注释中列出了实现步骤(最搞的是 You can do it! 注释),所以代码写起来也比较顺畅,以上~~文章来源地址https://www.toymoban.com/news/detail-474930.html

到了这里,关于CMU15445 (Fall 2020) 之 Project#1 - Buffer Pool 详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 缓存替换策略:LRU-K算法详解及其C++实现 CMU15-445 Project#1

    CMU 15-445 (FALL 2022) Project #1 Task#2 LRU-K 替换策略详解实现,尽量提供思路,也可以为其他同学实现LRU-K算法做参考 参考文献:The LRU-K page replacement algorithm for database disk buffering (acm.org) 在网上都找不到其他参考,只有这一篇1993年的论文 LRU-K替换策略 LRU-K是LRU算法的一种衍生。强烈

    2023年04月12日
    浏览(35)
  • 【cmu15445c++入门】(8)C++ 智能指针unique_ptr

    智能指针是一种数据结构,用于没有内置内存管理的语言(例如 C++)中的内存管理(有时还有其他功能)。内置内存管理的语言的指的是具有垃圾回收功能的语言,如Java、Python。 现代C++标准库的两个智能指针(以及您将在类中使用的智能指针)是 std::unique_ptr 和 std::shared_

    2024年02月20日
    浏览(43)
  • MySQL - Buffer Pool

    Buffer Pool 主要用于缓存数据库表的数据页,以提高数据库的读取性能: 缓存数据页 :Buffer Pool 是 MySQL 中用于缓存数据页的内存区域。数据页通常包含数据库表的数据,如行记录等。当查询或读取数据时,MySQL会首先查看Buffer Pool中是否已经缓存了相应的数据页。如果数据页在

    2024年02月07日
    浏览(38)
  • MySQL中的Buffer Pool

            Buffer Pool是数据库的一个内存组件,里面缓存了磁盘上的真实数据,然后我们的Java系统对数据库执行的增删改操作,其实主要就是对这个内存数据结构中的缓存数据执行的。我们先来看一下下面的图,里面就画了数据库中的Buffer Pool内存组件。         接着我

    2024年02月11日
    浏览(49)
  • 2-3-5-3、InnoDB 的 Buffer Pool

    对于使用 InnoDB 作为存储引擎的表来说,不管是用于存储用户数据的索引(包括聚簇索引和二级索引),还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是 InnoDB 对文件系统上一个或几个实际文件的抽象,也就是说数据说到底还是存储在磁盘上的

    2023年04月08日
    浏览(32)
  • 如何设置innodb_buffer_pool_size

    innodb_buffer_pool_size 是MySQL InnoDB存储引擎的一个重要参数,它决定了InnoDB存储引擎可以使用的内存缓存池的大小。合理的设置 innodb_buffer_pool_size 可以提高MySQL数据库的性能。以下是设置 innodb_buffer_pool_size 的步骤: 确认MySQL的版本: 在MySQL客户端中输入以下命令: 如果MySQL的版本

    2024年02月06日
    浏览(35)
  • 【MySQL自身的性能优化】InnoDB 的 Buffer Pool

    有近俩个来月没有写博客了,也不知道自己在瞎忙活什么。之前我有写过一篇关于 MySQL 的内存池一篇博客 InnoDB体系架构之内存池(buffer pool),那是看楠哥视频后总结出来的。然后现是看了《MySQL 是怎样运行的》一书,也可能是基础相比之前要好了些,理解的要好点了吧,就

    2024年01月20日
    浏览(39)
  • 【MySQL】change buffer,buffer pool,redo log,bin log,undo log的作用

    当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。 在下次查询需要访问这个数据页的时候,将数据页

    2024年02月16日
    浏览(40)
  • MySQL 8.0中InnoDB buffer pool size进度更透明

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源。 GreatSQL是MySQL的国产分支版本,使用上与MySQL一致。 作者:Yejinrong/叶金荣 文章来源:GreatSQL社区原创 MySQL 8.0 up up up~ 从MySQL 5.7开始,支持在线动态调整 innodb buffer pool,并为此新增了一个状态变量 Innod

    2024年02月02日
    浏览(33)
  • mysql8.0 性能优化配置 innodb_buffer_pool_size

    15.5.1 Buffer Pool 缓冲池是主内存中的一个区域,InnoDB在访问表和索引数据时会在该区域进行缓存。缓冲池允许直接从内存访问频繁使用的数据,这加快了处理速度。在专用服务器上, 通常会将高达80%的物理内存分配给缓冲池。 为了提高高容量读取操作的效率,缓冲池被划分为

    2024年02月09日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包