CMU-15445 2021 Project 1-BUFFER POOL (缓存池)
先贴结果图:
CMU禁止公开源代码哦~, 有问题欢迎私聊, 评论或者加群: 484589324交流~
Project1的内容是实现一个缓存池, 因为实验数据库是面向磁盘的数据库(这样才能提供海量存储), 而内存是很小的, 因此我们需要不断的在内存和磁盘中交换内容才能实现功能
一共有三个子实验, 难度大概是: 2 > 1 > 3
最核心的概念有:
- page(物理页), 数据库存储的原子(最小)单位, 也是磁盘和内存交换的最小单元, 磁盘上有海量的page, 由page_id唯一标识
- frame(内存帧), 帧和页的大小一致, 可以将内存看做一个由桢组成的数组, 实验中由pages_[frame_id].GetData()就可以获取到一个桢的地址, 帧的数量有限(内存很小), 由frame_id唯一标识, 一个帧在生命周期中会不断的被清空, 被写入别的page, 被刷盘等等
- pin_count(某个帧被多少个线程引用), 如果不为0, 则它一定不能在LRU中, 如果变为0了, 就将它存入LRU
1. LRU REPLACEMENT POLICY (LRU淘汰策略)
内存很小, 磁盘很大, 因此需要不断的去淘汰内存中的frame以写入磁盘中的page, 而在诸多淘汰算法中, 最经典的一定是最近最少使用(LRU)算法, 因为这种算法不会轻易淘汰热点数据, 客观来说比较合理
LRU算法也是面试中常考的内容, 建议在做实验之前先做一下这道题LeetCode146.LRU
做完之后, 相信对LRU基本的认知你已经拥有了, 接下来开始实验:
1-1. 讲解一下需求
需要实现的函数不多, 需求如下:
bool Victim(frame_id_t* f_id)
: 强制淘汰一帧, 将被淘汰的帧的id存入*f_id, 如果LRU为空无法淘汰返回false(这种情况不要更改 *f_id), 不为空淘汰成功了就返回truevoid Pin(frame_id_t f_id)
: 将某个帧固定在内存中, 不允许LRU淘汰它(也就是直接从LRU删除该帧) , 当然如果找不到该帧就不用删除了(也没法删除)void Unpin(frame_id_t)
: 某个帧没有线程引用了, 可以存入LRU等待被淘汰了Size()
: LRU中的帧数, 顾名思义即可
1-2. 注意点
- 实现思路和LeetCode一样, 双链表 + 哈希即可, 注意并发安全
- 注意Unpin的时候如果该frame已经在内存中了, 函数直接返回而不是将该frame更新到头部, 这一点和LeetCode不一样(比LeetCode更简单), 仔细想一下这样是合理的, 因为LRU中U的含义是使用, 只是Unpin并没有使用, 所以不应该对LRU的结构有影响
2. BUFFER POOL MANAGER INSTANCE (缓存池管理者实例)
实验1中最难的一项(也没有很难), 利用自己的LRU, 实际的去管理内存中的帧
2-1. 讲解一下需求
每个函数之间非常独立, 逐一击破即可:
Page* FetchPgImp(page_id)
: 从内存中抓取一页, 记得pin_count从0变1的时候要从LRU删除(调用pin)bool UnpinPgImp(page_id, is_dirty)
: 一个线程不再引用一个页了, 将其pin_count减少1, 并通知manager是否该页已经脏了, 有任何异常情况(例如pin_count已经为0了)就返回false, 记得pin_count变为0时调用LRU的unpinbool FlushPgImp(page_id)
: 将某一页刷盘, 无论是否dirty, 这是强制刷盘Page* NewPgImp(page_id)
: 不是通过磁盘刷进内存, 而是直接在内存中新建一页, 注意新页要第一时间写入到磁盘(尽管没什么内容), 以确保磁盘能感知到该页, 否则被LRU淘汰就完蛋了, 如果内存全是被pin的页就没办法了, 返回空指针bool DeletePgImp(page_id)
: 从磁盘中删除一页, 当然也要在内存中删除它了, 但是如果pin_count非0, 就不能删除, 而是返回falsevoid FlushAllPagesImpl()
: 最好欺负的一个, 挨个刷盘就行了
2-2. 注意点
-
挨个函数去实现即可, 多看Teacher的注释, Buffer Pool的函数之间不存在什么调用关系, 每一个功能都是独立的
-
注意并发安全, 锁的粒度可以很粗
-
实现完一个函数之后可以看看和我上面需求中的点是否比较符合了, 可能能避免一些坑
3. PARALLEL BUFFER POOL MANAGER (并行的缓存池管理者)
为了避免频繁争抢锁, 一个不错的idea是做多个BPM(Buffer Pool Manager), 根据page_id决定由哪个manager来管理, 这样抢锁的程度会低很多
实验中用的是简单的取模运算, 不是什么复杂的哈希算法, 即根据page_id取模bpm的数量来判断要分配到哪个bpm
3-1. 讲解一下需求 (忽略构造析构函数)
很简单, 如下:
GetPoolSize()
: 每个bpm的容量 * bpm的个数GetBufferPoolManager(page_id)
: 取模即可, 一行结束FetchPgImp(page_id)
: 一行UnpinPgImp(page_id, is_dirty)
: 一行FlushPgImp(page_id)
: 一行Page* NewPgImp(page_id)
: 从start_idx(类构造时初始为0)开始转一圈(转回到start_idx), 看看哪个bpm能New成功, 转了一圈都不行就返回空指针, 记得无论结果如果都将start_idx增加1并取模size, 记得加锁DeletePgImp(page_id)
: 一行FlushAllPagesImpl()
: 一个循环, 两三行
3-2. 注意点
-
start_idx会有并发问题, 应该是需要加个锁, 虽然数据好像比较弱不加也能过
-
调用自己之前的Buffer Pool即可, 这个真的不难