proj1_Buffer Pool
LRU-K,Disk_Scheduler,Buffer_Pool_Manager
Proj1:buffer_pool
概览
缓存池负责在内存和磁盘之间来回移动物理页,它使得数据库管理系统能够支持比系统可用内存更大的数据库。
- LRU-K
- 后向
K
距离(Backward K-Distance) - 后向
K
距离是衡量某个数据帧在缓存中被保留理由的一个重要指标。这个距离的计算方法是,找出当前时间戳与该数据帧第K
次之前访问的时间戳之间的差。简单地说,这个距离衡量了自从该数据帧最后一次成为第K次访问以来已过去的时间长度。 - 淘汰机制
- 在
LRU-K
算法中,使用后向K距离来决定哪个数据帧应当从缓存中淘汰: 如果一个数据帧的访问历史少于K次,那么它的后向K
距离被认为是无限大+inf
。这意味着这个数据帧相对来说是“新”的或者“不够重要的”,因为它还没被足够频繁地访问。 当多个数据帧有相同的无限大后向K
距离时,算法会淘汰那个整体上最早访问过的数据帧,即所有数据帧中记录的最早访问时间最早的那个。 - 淘汰流程的逻辑
- 计算每个数据帧的后向K距离:通过当前时间戳与第K次之前访问的时间戳之间的差分。 比较后向
K
距离: 找到拥有最大后向K
距离的数据帧(这代表它相对于其他数据帧而言,在很长时间内不太可能被访问)。 如果有多个数据帧的后向K
距离是+inf
(表示它们被访问的次数少于K次),那么这些数据帧被视为候选淘汰对象。 - 淘汰数据帧:
- 在所有候选淘汰对象中,淘汰那个整体上最早访问过的数据帧,确保缓存中保留的数据帧是最近被频繁访问的。
- 后向
ToDo
- LRU-K replacement policy
- Disk scheduler
Buffer pool manager
上述部分的类的API需要自己完成,不要修改任何类中定义的函数名。
不要删除类中的数据成员,可以添加数据成员和辅助函数
实现是要求线程安全的
Task1 LRU-K Replacement Policy
这个部分负责跟踪缓存池中页的使用,需要实现的类:LURKReplacer
位于src/include/buffer/lru_k_replacer.h
,需要实现的文件位于:src/buffer/lru_k_replacer.cpp
。
LURKReplacer
是一个单独的类,不和其他的Replacer
类相关联。不要实现LRU
或clock replacement policy
。
LRU-K
算法会驱逐替换器中所有帧里反向k距离最大的那个帧。反向k距离的计算方式是当前时间戳与第k次先前访问的时间戳之间的时间差。对于历史访问次数少于k次的帧,其反向k距离被赋予+inf
(无穷大)。当多个帧的反向k距离为+inf
时,替换器会驱逐具有最早总体时间戳的帧(即,其最近一次记录的访问是所有帧中总体上最近一次访问的帧)。
LRUKReplacer
的最大size
等于buffer_pool
的size
,因为它包含了BufferPoolManager
中所有帧的占位符。不过在任何给定时刻,不是所有的replacer
中的帧都视为可以被驱逐的。LRUKReplacer
的大小由可驱逐帧的数量表示。LURKReplacer
初始化时没有帧,因此,只有当有帧被标记为evictable
,replacer
的size
才会增加。
- TODO
src/include/buffer/lru_k_replacer.h
&src/buffer/lru_k_replacer.cpp
Evict(frame_id_t* frame_id)
:evict
一个在replacer
中用LURK
算法从所有evictable
的帧中得到的帧,将这个帧的id
存在输出参数里并且返回True
,如果没有evictable
的帧则返回false
RecordAccess(frame_id_t frame_id)
: 记录当前时间戳被访问的frame id
,这个方法应该在一个页被固定在bufferPoolManager后被调用。Remove(frame_id_t frame_id)
: 清楚一条特定帧的访问记录,此方法仅应在BufferPoolManager中页面被删除时调用。SetEvictable(frame_id_t frame_id, bool set_evictable)
:此方法控制一个帧是否可被驱逐。它还控制LRUKReplacer
的大小。在实现BufferPoolManager
时,你会知道何时调用此函数。具体来说,当一个页面的固定计数达到0时,其对应的帧被标记为可驱逐,并且替换器的大小增加。Size()
: 此方法返回当前在LRUKReplacer
中的evictable
帧的数量。
- LRU
- 用到了一个哈希表和一个双向链表实现,双向链表有一头一尾,分别用来代表最早和最晚access过的节点。
双向链表用到的函数:
1 2 3
void addNode(DoubleNode* newNode) {} // 加入一个节点到头部 void moveNodeToTail(DoubleNode* node) {} // 将指定节点移动到尾部 DoubleNode *removeHead() {} // 删除头节点(最近最少使用节点)
LRU_Cache用到的函数:
1 2
int get(int key) {} // access 指定节点 void put (int key, int value) {} // put 指定节点
- LRU-K
- 用到了一个history_list和一个buffer_list来分别记录access次数小于k的和access次数大于等于k的节点。 所以我们现在需要两个双向链表,并且它们的总容量等于buffer_pool的大小,有两种策略,一种是还是用一个双向链表,不过中间用一个mid指针分隔history和buffer;第二种策略就是用两个链表实例。
让我们来设计一下现在的LRUK:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class LRUKNode {
public:
size_t fid_; // 类似于键
size_t access_times; // 命中缓存的次数
bool is_evictable;
LRUKNode *next;
LRUKNode *prev;
}
class Double_list {
public:
LRUKNode *head;
LRUKNode *tail;
size_t evictable_num; // evictable 节点数
size_t node_num; // 总节点数
public:
void addNode(DoubleNode* newNode) {} // 加入一个节点到头部
void moveNodeToTail(DoubleNode* node) {} // 将指定节点移动到尾部
DoubleNode *removeHead() {} // 删除头节点(最近最少使用节点)
}
class LRUK_Cache {
public:
size_t replacer_size_; // 整个缓存池的容量
size_t curr_size_{0}; // evictable 节点数量
size_t node_num; // 全部节点的数量
size_t k_;
Double_list history_list; // 两个双向链表
Double_list buffer_list;
std::unordered_map<size_t, LRUKNode*> his_node_store_;
std::unordered_map<size_t, LRUKNode*> buf_node_store_;
public:
bool Evict(frame_id_t *frame_id) -> bool;
void RecordAccess(frame_id_t frame_id);
void SetEvictable(frame_id_t frame_id, bool set_evictable);
void Remove(frame_id_t frame_id);
size_t Size() -> size_t;
}
具体方法的实现
- Evict
- 判断
LRUK_Cache
的evictable_num
是否为0,是的话继续执行,否则返回false
。 - 先看
history
里有没有evictable
的节点,有的话就对history_list
做evict
,否则对buffer_list
做。 - 记录被
evict
的节点的frame_id
,释放这个节点的内存,整个LRUK_Cache
的evictable_num-1
。 - 返回
true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
auto LRUKReplacer::Evict(frame_id_t *frame_id) -> bool { if (curr_size_ == 0) return false; LRUKNode* node; if (his_list.evictable_num != 0) { node = his_list.remove(); } else { node = buf_list.remove(); } if (!node) return false; *frame_id = node->get_fid(); if (his_node_store_.find(node->get_fid()) != his_node_store_.end()) { his_node_store_.erase(node->get_fid()); } else if (buf_node_store_.find(node->get_fid()) != buf_node_store_.end()) { buf_node_store_.erase(node->get_fid()); } delete node; curr_size_--; node_num--; return true; }
- 判断
- RecordAccess
- 在
history_list
找到了:(FIFO)access_time+1 == k
那么就要把这个节点移动到buffer_list
里了。buffer_list
的node_num++
,如果这个节点还是evictable
的话那buffer_list
的evictable_num++
。history_list
的node_num
和evictable_num
也都要做相应的处理access_time+1 < k
那就access_time++
。
- 在
buffer_list
里找到了:(LRU)- 移动到尾部
- 对于新的
frame
:buffer_pool.size > node_num
-> 直接插入到history_list
中,LRUKCache.node_num++
,his_list.node_num++
。buffer_pool.size == node_num && evictable_num != 0
-> 做Evict
然后做addNode
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
void LRUKReplacer::RecordAccess(frame_id_t frame_id/*, [[maybe_unused]] AccessType access_type*/) { LRUKNode* node; if (his_node_store_.find(frame_id) != his_node_store_.end()) { // 如果在历史列表找到了,access_times++ node = his_node_store_[frame_id]; size_t times = node->get_access_times(); if (times + 1 >= k_) { node->set_access_times(times+1); LRUKNode *tmp = node; // 这里的顺序翻一下就全错了!!! his_list.remove_node(node); buf_list.add_node(tmp); buf_node_store_[frame_id] = tmp; his_node_store_.erase(frame_id); } else { node->set_access_times(times + 1); } } else if (buf_node_store_.find(frame_id) != buf_node_store_.end()) { // 如果在缓冲列表找到了,移动到尾部 node = buf_node_store_[frame_id]; buf_list.move_node_to_tail(node); // 同上 } else { // 如果是新帧,创建并加入历史列表 node = new LRUKNode(frame_id); node->set_access_times(1); his_node_store_[frame_id] = node; if (replacer_size_ > node_num) { his_list.add_node(node); node_num++; } else if (replacer_size_ == node_num && curr_size_ != 0) { frame_id_t id; Evict(&id); his_list.add_node(node); } } }
- 在
- SetEvictable
- 先在
history_list
里找,如果发现节点的Evictable
状态发生了变化那么就对LRUK_Cache
和history_list
的evictable_num
做处理。 buffer_list
同理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
void LRUKReplacer::SetEvictable(frame_id_t frame_id, bool set_evictable) { if (his_node_store_.find(frame_id) != his_node_store_.end()) { LRUKNode* node = his_node_store_[frame_id]; if (node->is_evictable() != set_evictable) { // 如果状态发生了变化 node->set_evictable(set_evictable); curr_size_ += set_evictable ? 1 : -1; if (set_evictable) { his_list.evictable_num++; } else { his_list.evictable_num--; } } } else if (buf_node_store_.find(frame_id) != buf_node_store_.end()) { LRUKNode* node = buf_node_store_[frame_id]; if (node->is_evictable() != set_evictable) { // 如果状态发生了变化 node->set_evictable(set_evictable); curr_size_ += set_evictable ? 1 : -1; if (set_evictable) { buf_list.evictable_num++; } else { buf_list.evictable_num--; } } } }
- 先在
Task2 Disk Scheduler
这个组件负责在磁盘管理器上调度读写操作。
磁盘调度器可以被其他组件(在这种情况下,是任务3中的缓冲池管理器)用来排队磁盘请求,这些请求由一个DiskRequest
结构体表示(已经在src/include/storage/disk/disk_scheduler.h
中定义)。磁盘调度器将维护一个后台工作线程,负责处理已调度的请求。
磁盘调度器将使用一个共享队列来安排和处理DiskRequests
。一个线程将请求添加到队列中,而磁盘调度器的后台工作线程将处理队列中的请求。我们已经在src/include/common/channel.h
中提供了一个Channel
类,以促进线程间数据的安全共享,但如果你认为有必要,也可以自由使用你自己的实现。
DiskScheduler
的构造函数和析构函数已经实现,负责创建和加入后台工作线程。
- TODO(only)
/include/storage/disk/disk_scheduler.h
src/storage/disk/disk_scheduler.cpp
Schedule(DiskRequest r)
: 安排一个请求给磁盘管理器执行。DiskRequest
结构体指定了请求是用于读取还是写入,数据应该写入何处或从何处读取,以及操作的页ID
。DiskRequest
还包括一个std::promise
,一旦请求被处理,其值应该被设置为true
。StartWorkerThread()
: 后台工作线程处理已调度请求的启动方法。工作线程在1DiskScheduler
构造函数中创建,并调用此方法。该方法负责获取队列中的请求并将其分派给DiskManager
。请记住在DiskRequest
的回调上设置值,以向请求发起者发出请求已完成信号。此方法不应返回,直到DiskScheduler
的析构函数被调用。
helper function in src/include/storage/disk/disk_manager.h
DiskManager::ReadPage()
DiskManager::WritePage()