Lab1 - Buffer Pool Manager
实验指导书
-
构建一个新的面向磁盘的存储管理器,这样的存储管理器假定数据库的主要存储位置在磁盘上。
-
在存储管理器中实现缓冲池。缓冲池负责将
pag从主存到磁盘来回移动。允许 DBMS 支持大于系统可用内存量的数据库。缓冲池的操作对系统中的其他部分是透明的。例如,系统使用其唯一标识符 (page_id_t)向缓冲池请求页面,但它不知道该页面是否已经在内存中,或者系统是否必须从磁盘中检索它。 -
实现需要是线程安全的。多个线程将同时访问内部数据结构,因此需要确保临界区受到
latches的保护。 -
需要在存储管理器中实现以下两个任务:
(1)LRU 替换原则
(2)缓冲池管理器
-
任务 1 - LRU 替换策略
(1)该组件负责跟踪缓冲池中的页面使用情况。将在 src/include/buffer/lru_replacer.h 中实现一个新的子类
LRUReplacer,它相应的实现文件在 src/buffer/lru_replacer.cpp 。LRUReplacer继承了抽象类Replacer(src/include/buffer/replacer.h)。(2)
LRUReplacer的大小与BufferPoolManager相同,因为在BufferPoolManager中包含了所有frames的placeholders。 但是,并非所有frames都被视为LRUReplacer. 将LRUReplacer被初始化为有没有frames。然后,LRUReplacer将只考虑新的没有划分的那些。(3)需要实现课程中讨论的
LRU策略。您将需要实现以下方法:Victim(T*):Replacer跟踪与所有元素相比最近访问次数最少的对象并删除,将其删除页号存储在输出参数中,并返回True。如果Replacer为空则返回False。Pin(T):在将page固定到BufferPoolManager中的frame之后,应该调用此方法。它应该从LRUReplacer中删除固定包含固定page的frame。Unpin(T):当页面的引用计数变为 0 时,应该调用此方法。这个方法应该将未包含固定page的frame添加到LRUReplacer。(注意,需要判断是否超出了内存大小,如果超过了,则删除较新的页面,然后再添加。)Size():这个方法返回当前在LRUReplacer中的页面数。
-
任务 2 - 缓冲池管理器
- 接下来,需要在系统中实现缓冲池管理器 (
BufferPoolManager)。该BufferPoolManager负责从DiskManager中读取数据库页并将它们存储在内存中。BufferPoolManager还可以在明确指示或需要为新页腾出空间时,将脏页写入磁盘。 - 为了确保实现与系统的其余部分一起正常工作,提供了一些已经填写好的功能。也不需要实现实际读写数据到磁盘的代码(在给出的实现中称为
DiskManager)。 - 系统中所有的内存页面都由
Page对象表示。BufferPoolManager并不需要了解这些页的内容。但是,作为系统开发人员,重要的是要理解Page对象只是缓冲池中用于存储内存的容器,因此不是特定于唯一的页面。也就是说,每个Page对象都包含一个内存块,DiskManager将它用作复制从磁盘读取的page内容的位置。当它来回移动到磁盘时,BufferPoolManager将重用相同的Page对象来存储数据。这意味着相同的Page在系统的整个生命周期中可能包含不同的物理页面。Page对象的标识符 (page_id) 跟踪它所包含的物理页面,如果Page对象不包含物理页,则必须将其page_id设置为INVALID_Page_id。 - 每个
Page对象还维护了一个计数器,用于表示 “固定” 该页面的线程数。BufferPoolManager不允许释放被 ”固定“ 的页面。每个Page对象还跟踪它标记的脏页。我们需要判断页面在解除绑定之前是否被修改过。BufferPoolManager必须将dirty Page的内容写回磁盘,然后才能重用该对象。 BufferPoolManager的实现将使用上述步骤中创建的LRUReplacer类。它将使用LRUReplacer来跟踪Page对象被访问的时间,以便在必须释放frame来腾出空间给从磁盘复制的新物理页时决定删除哪个对象。- 需要实现头文件(src / compress / buffer_pool_pool_pool.h)中定义的在源文件 (src / buffer / buffer_pool_manager.cpp) 中的下列函数:
FetchPageImpl(page_id)NewPageImpl(page_id)UnpinPageImpl(page_id, is_dirty)FlushPageImpl(page_id)DeletePageImpl(page_id)FlushAllPagesImpl()
- 对于
FetchPageImpl,如果空闲列表中没有页面可用,并且所有其他页面当前都被固定,则应该返回NULL。不管FlushPageImpl的引用状态如何,都应该刷新页面。
-
测试
-
LRUReplacer:test/buffer/lru_replacer_test.cpp
-
BufferPoolManager:test/buffer/buffer_pool_manager_test.cpp
-
本地测试:
// 别忘了删除测试中的 disabled $ mkdir build $ cd build $ make lru_replacer_test $ ./test/lru_replacer_test $ mkdir build $ cd build $ make buffer_pool_manager_test $ ./test/buffer_pool_manager_test // 检查 $ cd build $ make format $ make check-lint $ make check-clang-tidy -
-
提交:
-
src/include/buffer/lru_replacer.h
-
src/buffer/lru_replacer.cpp
-
src/include/buffer/buffer_pool_manager.h
-
src/buffer/buffer_pool_manager.cpp
-
打包
// 下面的写在一行,空格分隔。最好是写个bash脚本,比较方便。 zip project1.zip src/include/buffer/lru_replacer.h src/buffer/lru_replacer.cpp src/include/buffer/buffer_pool_manager.h src/buffer/buffer_pool_manager.cpp
-
设计思路
LRU(Least-Recently Used)的策略:- 数据一般是存储在磁盘上的,当我们需要读取一个数据的时候,会首先把它加载到内存中,然后返回给客户端。
- 因为一般内存比磁盘小,容量有限,不可能同时存储那么多的数据。因此,内存会时常把暂时不需要的页面换回磁盘中,等到调用的时候再次置换回来。
LRU正是页面置换算法的一种,最近最少使用的策略,它有一个潜在的假设,如果某个页的数据被访问过一次,那么下次再被访问的机率也就更高。
LRU的思路:- 首先,页面需要使用一个数据结构来存储,考虑到它需要不断的插入和删除,特别是需要头插和尾删,因此使用双向链表是比较方便的。另外,可以增加一个哈希表来加快查找和定位的速度(O(1))。综合考虑,使用双向链表 + Hash Table 来完成比较合适。
- 双向链表负责维护一个页面的集合,数据按照从最近使用过的到最近没有使用的来排序。因此,每当更新某个页面的时候,都应该把它放在双向链表的头部,即使是某个页面已经出现在链表中,也要拿出来,重新放置。每当置换的时候,都应该从双向链表的尾部置换页面,取出最近没有使用的页面。
- Hash Table 负责维护一个从页面到链表节点的映射。它的作用有两个,一个是方便查找某个页面当前是否在这个链表当中。另一个是能够快速定位到当前页面在链表中的位置,方便删除操作。
Clock策略:Clock也是页面置换算法的一种。Clock置换算法分为两种,一种是简单的置换算法,与LRU算法类似。另一种是改进型的,相比于前一种,减少了磁盘IO,性能更加高效。- 简单
Clock的思想是先将内存中的所有页面想象成一个环形队列,通过维护一个访问位,每次更新的时候,如果访问位为 0,表示最近没有被访问,则可以置换;否则,将访问位置 0,继续寻找。 - 改进版的思想是,需要添加一个修改位,修改了的置 1 ,没有则置 0 。那么当要置换时,(访问位,修改位)可能的组合按优先级分为以下四种:(0,0)、(1,0)、(0,1)、(1,1)。因此,它的执行过程是:(1)循环扫描查找(0,0)。有则退出。如果访问位为1则置 0 。(2)循环扫描查找(0,1)。有则退出。如果访问位为1则置 0 。(3)重复第一步。
Clock设计思路:- 这里只考虑简单的
Clock设计思路。维护一个访问位数组和一个指针。每次当成环形数组循环查找当前需要被置换的页面。
- 这里只考虑简单的
Coding
C++ :std::scoped_lock 能够避免死锁的发生。它的构造函数能够自动进行上锁操作,析构函数会对互斥量进行解锁操作。能够保证线程安全。
-
根据上面
LRU的设计思路,我们可以定义出LRUReplacer的数据结构,如下。class LRUReplacer : public Replacer { private: // TODO(student): implement me! // 为了线程安全需要加的锁 std::mutex latch; // 这个表示 LRUReplacer 的大小,Buffer Pool大小相同。 size_t capacity; // 我们使用 双向链表 + 哈希表 的数据结构。 std::list<frame_id_t> LRUList; // 使用 unordered_map(注意加头文件),从 frame_id 映射到 Node。 std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> LRUHash; }; -
根据上面
Clock的设计思路,我们可以定义出ClockReplacer的数据结构,如下。class ClockReplacer : public Replacer { private: // TODO(student): implement me! // 对于每个 frame_id,都需要标记两个元素。isPin 表示当前 frame 是正在被引用。 // ref 表示当前的 frame 最近是否被使用过。 struct clockItem { bool isPin; bool ref; }; // 为了线程安全需要加的锁 std::mutex latch; // 这个表示 ClockReplacer 的大小,Buffer Pool大小相同。 size_t victim_number; // clock 指针当前所指位置。 size_t clockHand; // clockItem 数组,数组下表表示 frame_id。 std::vector<clockItem> victimArray; }; -
Replacer:追踪page使用情况的抽象类。类中不包含page的具体信息,只是为Buffer Pool Manager服务,提供了一个可以替换的frame_id。这个类包含了四个主要的函数。这里为了方便,将两个实现LRUReplacerClockReplacer根据功能写在了一起,看的时候如果不舒服可以一次看一种实现方法。class Replacer { public: Replacer() = default; virtual ~Replacer() = default; /** * 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 */ virtual bool Victim(frame_id_t *frame_id) = 0; /** * Pins a frame, indicating that it should not be victimized until it is unpinned. * @param frame_id the id of the frame to pin */ virtual void Pin(frame_id_t frame_id) = 0; /** * Unpins a frame, indicating that it can now be victimized. * @param frame_id the i

本文档详细介绍了数据库管理系统中缓冲池管理器的实现,包括LRU和Clock页面替换策略。LRU策略使用双向链表和哈希表来跟踪最近最少使用的页面,而Clock策略则通过一个带有访问位和修改位的环形数组来选择可替换的页面。BufferPoolManager负责读取和写入磁盘页面,同时使用LRU或Clock策略进行页面置换。测试用例包括LRUReplacer和BufferPoolManager的单元测试,确保了线程安全和正确性。
最低0.47元/天 解锁文章
2012

被折叠的 条评论
为什么被折叠?



