文章目录
- Task1 LRU-K 替换策略
- Task2 缓冲池管理
- Task3 读/写页面保护
Task1 LRU-K 替换策略
LRU-K算法:当访问次数达到K次后,将数据索引从历史队列移到缓存队列中(缓存队列时间降序);缓存数据队列中被访问后重新排序;需要淘汰数据时,淘汰缓存队列中排在末尾的数据。
相比于LRU-1,缓存数据更不容易被替换,而且偶发性的数据不易被缓存。在保证了缓存数据纯净的同时还提高了热点数据命中率。
一般来说通过以下数据结构实现:
左边队列是优先淘汰的访问历史队列,一有数据被访问就加入该队列,若此后没被访问到K次,且需要淘汰访问,就先入先出地淘汰掉。若访问到了K次,就进入右侧缓存数据队列,若再被访问就移到队尾,这样若此后没在被访问就会被先进先出地淘汰掉。淘汰历史是访问历史队列优先检查是否有可淘汰的历史,若没有则再看缓存数据队列是否有。
该代码实现的LRU-K替换算法是通过将一条双向链表分割成两部分来完成功能的
LRUKNode *head_;
LRUKNode *medium_;
LRUKNode *tail_;
medium–tail是访问历史队列,head–medium是缓存数据队列,链表上每个结点有一个标志位LRUKNode::is_evictable_
来标志是否是可以被淘汰的
帧与队列节点的映射是通过无序map实现的
std::unordered_map<frame_id_t, LRUKNode *> node_store_;
std::lock_guard<std::mutex> lock(latch_);//std::mutex latch_;
通过"资源分配时初始化"(RAII)方法来加锁、解锁,避免了在临界区中因为抛出异常或return等操作导致没有解锁就退出的问题,lock_guard的作用域与普通对象相同,可以在一个方法中使用多次
Task2 缓冲池管理
页和帧是对同一数据在不同位置的不同称呼。页是在磁盘上存储的数据块,而帧是在内存中存储的数据块。当需要访问一个页时,DBMS会将该页加载到一个帧中(帧保存在缓冲池里),以便在内存中进行快速访问和修改。因此,页和帧实际上是同一数据在不同位置的两种表示方式。
Page *pages_;//缓冲池页面数组,虽然叫pages_但是保存的是帧,下标是frame_id
std::unordered_map<page_id_t, frame_id_t> page_table_;//页和帧的映射关系,通过page_id找frame_id,从而在pages_中找到内存缓冲池中的页数据
std::list<frame_id_t> free_list_;//记录哪些帧(pages_的下标)是空闲状态,BufferPoolManager初始化时全部帧都在这里
std::unique_ptr<LRUKReplacer> replacer_;//进行LRU-K策略的成员,传入frame_id表示对帧进行访问
BufferPoolManager::NewPage()
先从free_list_
中找空闲帧,有则弹出,没有就调用replacer_
的LRU-K策略淘汰一个帧,在page_table_
中取消映射,若淘汰的帧对应的页是脏页,就写入硬盘。然后分配page_id并将帧id交给page_table_
来构建映射,从硬盘读数据到pages_
并设置id,调用replacer_
的LRU-K策略添加对帧id的访问历史并设为不可淘汰,以防止使用时淘汰。
BufferPoolManager::FetchPage()
先从page_table_
缓冲池中找页,找不到就找free_list
中的空闲帧,没有就先replacer_
淘汰一个(淘汰访问历史、若为脏页就将pages_
中帧写入硬盘、删除page_table_
中的页帧映射)后用淘汰的页。获取页帧后配置pages_
中的页id并读取硬盘中页数据,构建页帧映射。将该页帧加入replacer_
的LRU-K策略队列并设置不可淘汰。比BufferPoolManager::NewPage()
多找了page_table_
BufferPoolManager::UnpinPage()
用于设置page_table_
中指定页映射的帧可淘汰
BufferPoolManager::FlushPage()
将pages_
中指定页id的帧数据写入硬盘
Task3 读/写页面保护
在缓冲池管理器中,FetchPage 和 NewPage 函数返回指向已经固定的页面的指针。固定机制确保页面不会被驱逐,直到页面上没有更多的读写操作。为了表明内存中不再需要该页面,程序员必须手动调用 UnpinPage。
另一方面,如果程序员忘记调用 UnpinPage,页面将永远不会被驱逐出缓冲池。后果是由于缓冲池现在使用更少的帧进行操作,因此将会有更多的页面进出磁盘。不仅性能会受到影响,漏洞也很难被检测到。
你将实现 BasicPageGuard,它存储指向 BufferPoolManager 和 Page 对象的指针。页面保护确保一旦 page 对象超出作用域,就在相应的 page 对象上调用 UnpinPage。注意,它仍然应该为程序员提供一个方法来手动解除页面的固定。
在未来的项目中,多个线程将对同一个页面进行读写,因此需要使用读写锁来确保数据的正确性。请注意,在 Page 类中,有用于此目的的相关方法。类似于页面的解除锁定,程序员在使用页面后可能会忘记解除锁定。为了缓解这个问题,你将实现 ReadPageGuard 和 WritePageGuard,它们会在页面超出范围时自动解锁。
BasicPageGuard
类成员属性有:
BufferPoolManager *bpm_{nullptr};
Page *page_{nullptr};
bool is_dirty_{false};
可见是对BufferPoolMananger
和Page
的进一步封装管理,ReadPageGuard
和WritePageGuard
是BufferPoolMananger
的好友类。
BasicPageGuard::Drop()
保证在相应 page 对象上调用 UnpinPage。同时在Drop()
和operator=()
中若封装的页非空就解读/写锁,而在有读写保护版的FetchPage()
,即BufferPoolMananger::FetchPageRead()
和BufferPoolManager::FetchPageWrite()
中进行加锁
return reinterpret_cast<T *>(GetDataMut());
//强制类型转换运算符 <要转换到的类型> (待转换的表达式)
static_cast
用于进行比较"自然"和低风险的转换,如整型和浮点型、字符型之间的互相转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如 T 是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。
reinterpret_cast
用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。
const_cast
运算符仅用于进行去除 const 属性的转换,它也是四个强制类型转换运算符中唯一能够去除 const 属性的运算符。
dynamic_cast
是通过"运行时类型检查"来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。
C++强制类型转换运算符(static_cast、reinterpret_cast、const_cast和dynamic_cast) (biancheng.net)