15445 数据库系统(24Fall实验)

目前进度,P0 ,P1 完成

可能不会包含详细的代码逻辑,只是对错误和易错的地方进行总结,对在探索过程中获得的经验进行分享。

Prepare

Lab 源码仓库
GitHub - cmu-db/bustub: The BusTub Relational Database Management System (Educational)
你可以从 releases 下载对应的源码

课程首页
CMU 15-445/645 :: Intro to Database Systems (Fall 2024)

前置课程总结请看之前文章。

环境配置略,由于之前有现成的几乎没有配置()
clangd+lldb/gdb+clang vscode 一套反正是。
上班用的 clion 也很爽(现在还免费了)。
Pasted image 20250524151956.png

碎碎念

  • 不能理解的时候可以根据 test 样例来模拟
  • discord 有很多有用的讨论(百思不得其解的时候来看看吧
  • 建议关键路径打些日志,不过日志有时候也会导致错误
  • 提交的时候需要运行 python3 gradescope_sign.py 来签名

P1 Buffer Pool Manager

可以参考的文章

# CMU Fall 2024 15-445 P1&P2[优化P2]

T1

1. 访问次数达 K 次的帧

  • 驱逐策略:根据后向 k 距离(Backward K-Distance)决定,即当前时间戳与第 K 次历史访问时间戳的差值,选择后向 k 距离最大的帧进行驱逐。这类似于传统 LRU 的变种,但关注的是第 K 次访问的时间间隔,而非最近一次访问。
  • 示例:若某帧的访问历史为时间戳 [t1, t2, t3](K=3),当前时间为 t4,则后向 k 距离为 t4 - t1。距离越大,越优先被驱逐。

2. 访问次数未达 K 次的帧

  • 驱逐策略:==FIFO ==(LRU 在测试过程中会有问题)
  • 示例:若两个帧的访问次数均不足 K 次,且访问时间分别为 t1t2t1 < t2),则优先驱逐时间戳为 t1 的帧。

一个测试样例,搬运工。

TEST(LRUKReplacerTest, SecondSimpleTest) {
  bustub::LRUKReplacer lru_replacer(7, 3);
  // [4, 3, 2, 1]
  lru_replacer.RecordAccess(1);
  lru_replacer.RecordAccess(2);
  // 3的最早访问时间
  lru_replacer.RecordAccess(3);
  // 4的最早访问时间
  lru_replacer.RecordAccess(4);
  // [4, 1, 2, 3]
  lru_replacer.RecordAccess(1);
  lru_replacer.RecordAccess(2);
  lru_replacer.RecordAccess(3);
  // [4, 3, 1, 2]
  lru_replacer.RecordAccess(1);
  lru_replacer.RecordAccess(2);
  // 把1,2,3,4都标记为Evictable
  lru_replacer.SetEvictable(1, true);
  lru_replacer.SetEvictable(2, true);
  lru_replacer.SetEvictable(3, true);
  lru_replacer.SetEvictable(4, true);
  // 此时4被访问过1次,3被访问过2次, 4和3的访问次数都小于k
  // 但是3的第一次访问时间早于4, 所以被淘汰的应该是3,而不是4
  auto frame_id = lru_replacer.Evict();
  ASSERT_EQ(3, *frame_id);
}

注意不要修改 DiskRequst 结构…不然测试会 failed。
还想异步刷盘来着

T2

构造函数初始一个线程StartWorkerThread 根据Channel(线程安全的消息队列)取出DiskRequest,根据消息类型调用DiskManager 进行读写

T3

坑多到无力吐槽,理解能力太差,有些我一时真没理解它要做什么…

第一次提交,只有惨淡的47.0 分…
笔者实习多是追求高性能的简单并发,上锁的机会比较少,追求无锁更多…

菜就多练()

错误

部分错误是跟着其他错误一块出现的,只解释最后一次出现的解决方案

LRUKReplacerTest.Evict (0/4)
访问次数未达 K 次的帧 使用 FIFO

LRUKReplacerTest.ConcurrencyTest (0/5)
忘记上锁

BufferPoolManagerTest.PagePinHardTest (0/4)
BufferPoolManagerTest.DeletePageTest (0/4)
忘记replacer_->Remove…

BufferPoolManagerTest.IsDirtyTest (0/4)
WritePage Drop无需Flush,无需设置Dirty 为true

BufferPoolManagerTest.ContentionTest (0/2)
打日志导致并发错误?

BufferPoolManagerTest.EvictableTest (0/3)
BufferPoolManagerTest.ConcurrentWriterTest (0/5)
死锁 
Program exited with -1 in XXXs (timed out after XX secs, force kill)
统一归类

BufferPoolManagerTest.ConcurrentReaderWriterTest (0/6)
Frame 驱逐。

BufferPoolManagerTest.StaircaseTest (0/4)
1、判断过时,导致同时进入多个线程。
2、FetchPage不能加锁?

思考

和 23Fall 的区别在于需要 bpm_latch + frame_latch 结合使用。
对于上锁简单的总结:

  • 死锁避免:
    • 死锁的四个必要条件:互斥、不可抢占,占有且等待和循环等待。前两个破坏起来比较麻烦,而后两个就相对简单。
    • 如果一时间只占用一个锁,不会死锁,我们更应该思考是否正确。
    • 两个线程,一个持有 A 想要 B,一个持有 B 想要 A 的情况是典型的顺序问题。
    • 有时候上多个锁是必须的。
    • 多次上锁,多次解锁请使用递归锁。在这个 Task 我建议是 bpm 使用递归锁,因为线上测试样例你永远无法预测…避免使用递归锁请给每个函数创建一个上锁和无锁版本。
  • 正确性:
    • 对于可能不止一个线程读写的数据我们需要加锁保护。
    • 对于不同的两个锁切换过程中,考虑数据是否发生变化。例如判断条件是否过时,被驱逐,被改变都会导致错误。对于这种情况,我们可以延长大锁的时候,获取两个锁。也可以在第二个锁的期间再进行一次判断。
    • 当然也可以通过逻辑的方式来避免,例如一个锁的时候设置不能驱逐等
  • 思考不同锁的“作用”,做到清晰的上锁。
  • 建议除了官方给的函数,自己的辅助函数。尽量避免内部上锁,而让锁在上层控制。

来自蠢猪的例子:

auto BufferPoolManager::CheckedWritePage(page_id_t page_id, AccessType access_type) -> std::optional<WritePageGuard> {
  auto frame_opt = GetFrameByPageId(page_id);
  if (!frame_opt.has_value()) {
    frame_opt = ShouldLoadPage(page_id);
    if (!frame_opt.has_value()) {
      return std::nullopt;
    }
  }
  return WritePageGuard(page_id, frame_opt.value(), replacer_, bpm_latch_);
}

这里有两个错误。
1、通过 GetFrameByPageId 判断有无,如果无会 ShouldLoadPage,包含了两个操作,找到新的 Frame(驱逐或找到空闲),以及从磁盘加载。但是可能其他线程重复进入这块语句。(正常情况下是我从磁盘加载后再次判断就已经存在那个 Page 了),也就是这条语句我必须视为一个原子的,后面的操作影响到了我前面的判断。
2、在获取或加载 Frame 后,我的 Frame 可能被驱逐,我加锁的 Frame 对应的 Page 已经不是原本的那个(ntr

对于 1 上 bpm_latch,
对于 2 提前 replacer_->SetEvictable(frame->frame_id_, false)
然后部分驱逐后不影响操作的可以直接 frame->page_id_ != page_id 结束

tips

  • PageGuard is_valid_是在右值移动过程使用的,防止锁的一些问题,比如解锁两次
  • GetDataMut 需要SetDirty = true
  • disk_scheduler_->Schedule is_write_=false 的时候上的是独占锁…有点容易出错,其他时候读锁就可以
posted @ 2025-05-24 15:21  Fsyrl  阅读(137)  评论(0)    收藏  举报