LRU-K

文章讲述了LRU策略如何通过哈希表和双向链表在BufferPoolManager中管理内存,包括page的分配、回收以及脏页的处理。主要介绍了newpage和fetchpage操作,涉及内存分配、磁盘I/O和LRU替换算法的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LRU策略可以由哈希表加双向链表的方式实现,其中链表充当队列的功能以记录页面被访问的先后顺序,哈希表则记录<页面ID - 链表节点>键值对,以在O(1)复杂度下删除链表元素。实际实现中使用STL中的哈希表unordered_map和双向链表list,并在unordered_map中存储指向链表节点的list::iterator

page_table_用于保存磁盘页面IDpage_id和槽位IDframe_id_t的映射。

Buffer Pool Manager 则是向系统提供了获取 page 的接口。系统拿着一个 page_id 就可以向 Buffer Pool Manager 索要对应的 page,而不关心这个 page 具体存放在哪。系统并不关心(也不可见)获取这个 page 的过程,无论是从 disk 还是 memory 上读取,还是 page 可能发生的在 disk 和 memory 间的移动。这些内部的操作交由 Buffer Pool Manager 完成。

Buffer Pool Manager 里有几个重要的成员:

  • pages:buffer pool 中缓存 pages 的指针数组
  • disk_manager:可以用来读取 disk 上指定 page id 的 page 数据,或者向 disk 上给定 page id 对应的 page 里写入数据。(DiskManager负责数据库中页面的分配和回收。它执行从磁盘读取和写入页面的操作,在数据库管理系统的上下文中提供一个逻辑文件层。)将日志内容写入磁盘文件,
  • page_table: Extendible Hash Table,用来将 page id 映射到 frame id,即 page 在 buffer pool 中的位置
  • replacer:刚才实现的 LRU-K Replacer,在需要驱逐 page 腾出空间时,告诉我们应该驱逐哪个 page。
  • free_list:空闲的 frame 列表

Buffer Pool Manager 给上层调用者提供的两个最重要的功能是 new page 和 fetch page。

New Page

上层调用者希望新建一个 page,调用 NewPgImp

如果当前 buffer pool 已满并且所有 page 都是 unevictable 的,直接返回。否则:

  • 如果当前 buffer pool 里还有空闲的 frame,创建一个空的 page 放置在 frame 中。
  • 如果当前 buffer pool 里没有空闲的 frame,但有 evitable 的 page,利用 LRU-K Replacer 获取可以驱逐的 frame id,将 frame 中原 page 驱逐,并创建新的 page 放在此 frame 中。驱逐时,
  • 如果当前 frame 为 dirty(发生过写操作),将对应的 frame 里的 page 数据写入 disk,并重置 dirty 为 false。清空 frame 数据,并移除 page_table 里的 page id,移除 replacer 里的引用记录。
  • 如果当前 frame 不为 dirty,直接清空 frame 数据,并移除 page_table 里的 page id,移除 replacer 里的引用记录。

在 replacer 里记录 frame 的引用记录,并将 frame 的 evictable 设为 false。因为上层调用者拿到 page 后可能需要对其进行读写操作,此时 page 必须驻留在内存中。

使用 AllocatePage 分配一个新的 page id(从0递增)。

auto BufferPoolManagerInstance::AllocatePage() -> page_id_t { return next_page_id_++; }

将此 page id 和存放 page 的 frame id 插入 page_table。

page 的 pin_count 加 1。

 76 Page *BufferPoolManagerInstance::NewPgImp(page_id_t *page_id) {
 77   // 0.   Make sure you call AllocatePage!
 78   // 1.   If all the pages in the buffer pool are pinned, return nullptr.
 79   // 2.   Pick a victim page P from either the free list or the replacer. Always pick from the free list first.
 80   // 3.   Update P's metadata, zero out memory and add P to the page table.
 81   // 4.   Set the page ID output parameter. Return a pointer to P.
 82   frame_id_t new_frame_id;
 83   latch_.lock();
 84   if (!free_list_.empty()) {
 85     new_frame_id = free_list_.front();
 86     free_list_.pop_front();
 87   } else if (!replacer_->Victim(&new_frame_id)) {
 88     latch_.unlock();
 89     return nullptr;
 90   }
 91   *page_id = AllocatePage();
 92   if (pages_[new_frame_id].IsDirty()) {
 93     page_id_t flush_page_id = pages_[new_frame_id].page_id_;
 94     pages_[new_frame_id].is_dirty_ = false;
 95     disk_manager_->WritePage(flush_page_id, pages_[new_frame_id].GetData());
 96   }
 97   page_table_.erase(pages_[new_frame_id].page_id_);
 98   page_table_[*page_id] = new_frame_id;
 99   pages_[new_frame_id].page_id_ = *page_id;
100   pages_[new_frame_id].ResetMemory();
101   pages_[new_frame_id].pin_count_ = 1;
102   replacer_->Pin(new_frame_id);
103   latch_.unlock();
104   return &pages_[new_frame_id];
105 }

Fetch Page

上层调用者给定一个 page id,Buffer Pool Manager 返回对应的 page 指针。调用 FetchPgImp

假如可以在 buffer pool 中找到对应 page,直接返回。

否则需要将磁盘上的 page 载入内存,也就是放进 buffer pool。

107 Page *BufferPoolManagerInstance::FetchPgImp(page_id_t page_id) {
108   // 1.     Search the page table for the requested page (P).
109   // 1.1    If P exists, pin it and return it immediately.
110   // 1.2    If P does not exist, find a replacement page (R) from either the free list or the replacer.
111   //        Note that pages are always found from the free list first.
112   // 2.     If R is dirty, write it back to the disk.
113   // 3.     Delete R from the page table and insert P.
114   // 4.     Update P's metadata, read in the page content from disk, and then return a pointer to P.
115   frame_id_t frame_id;
116   latch_.lock();
117   if (page_table_.count(page_id) != 0U) {
118     frame_id = page_table_[page_id];
119     pages_[frame_id].pin_count_++;
120     replacer_->Pin(frame_id);
121     latch_.unlock();
122     return &pages_[frame_id];
123   }
124 
125   if (!free_list_.empty()) {
126     frame_id = free_list_.front();
127     free_list_.pop_front();
128     page_table_[page_id] = frame_id;
129     disk_manager_->ReadPage(page_id, pages_[frame_id].data_);
130     pages_[frame_id].pin_count_ = 1;
131     pages_[frame_id].page_id_ = page_id;
132     replacer_->Pin(frame_id);
133     latch_.unlock();
134     return &pages_[frame_id];
135   }
136   if (!replacer_->Victim(&frame_id)) {
137     latch_.unlock();
138     return nullptr;
139   }
140   if (pages_[frame_id].IsDirty()) {
141     page_id_t flush_page_id = pages_[frame_id].page_id_;
142     pages_[frame_id].is_dirty_ = false;
143     disk_manager_->WritePage(flush_page_id, pages_[frame_id].GetData());
144   }
145   page_table_.erase(pages_[frame_id].page_id_);
146   page_table_[page_id] = frame_id;
147   pages_[frame_id].page_id_ = page_id;
148   disk_manager_->ReadPage(page_id, pages_[frame_id].data_);
149   pages_[frame_id].pin_count_ = 1;
150   replacer_->Pin(frame_id);
151   latch_.unlock();
152   return &pages_[frame_id];
153 }

flushPgImp用于显式地将缓冲池页面写回磁盘。首先,应当检查缓冲池中是否存在对应页面ID的页面,如不存在则返回False;如存在对应页面,则将缓冲池内的该页面的is_dirty_置为false,并使用WritePage将该页面的实际数据data_写回磁盘。

 51 bool BufferPoolManagerInstance::FlushPgImp(page_id_t page_id) {
 52   // Make sure you call DiskManager::WritePage!
 53   frame_id_t frame_id;
 54   latch_.lock();
 55   if (page_table_.count(page_id) == 0U) {
 56     latch_.unlock();
 57     return false;
 58   }
 59   frame_id = page_table_[page_id];
 60   pages_[frame_id].is_dirty_ = false;
 61   disk_manager_->WritePage(page_id, pages_[frame_id].GetData());
 62   latch_.unlock();
 63   return true;
 64 }

### LRU-K 缓存算法概述 LRU-K 是一种扩展的缓存淘汰策略,其中 K 表示在判断一个对象是否为最近使用时考虑的历史访问次数。相比于传统的 LRU(K=1),LRU-K 能够更好地处理局部性和随机访问模式。具体来说,LRU-K 记录每个缓存项在过去 K 次访问中的时间戳,并根据这些历史信息决定淘汰顺序。 以下是基于 C 语言实现的一个简化版本的 LRU-K 缓存算法--- ### 数据结构定义 为了支持高效的插入、删除和查询操作,可以结合哈希表和双向链表来构建缓存系统。以下是核心数据结构的设计[^3]: ```c #include <stdio.h> #include <stdlib.h> #define CACHE_SIZE 5 #define MAX_KEY_LEN 20 typedef struct CacheNode { char key[MAX_KEY_LEN]; int value; int access_count; // 当前节点的访问计数 (用于 LRUK 的 K 值) double last_access_time; // 上次访问的时间戳 struct CacheNode* prev; struct CacheNode* next; } CacheNode; typedef struct LRUCache { CacheNode* head; CacheNode* tail; size_t count; } LRUCache; // 辅助函数声明 CacheNode* create_node(const char* key, int value); void add_to_head(LRUCache* cache, CacheNode* node); void remove_node(CacheNode* node); void move_to_head(LRUCache* cache, CacheNode* node); int get_cache_value(LRUCache* cache, const char* key); void set_cache_value(LRUCache* cache, const char* key, int value); ``` --- ### 核心逻辑实现 #### 创建新节点 创建一个新的缓存节点并初始化其属性: ```c CacheNode* create_node(const char* key, int value) { CacheNode* new_node = malloc(sizeof(CacheNode)); strncpy(new_node->key, key, MAX_KEY_LEN - 1); new_node->value = value; new_node->access_count = 1; // 初始化访问计数为 1 new_node->last_access_time = time(NULL); // 设置当前时间为初始时间戳 new_node->prev = NULL; new_node->next = NULL; return new_node; } ``` #### 将节点添加到头部 将指定节点移动到链表头位置,表示它是最新访问过的节点: ```c void add_to_head(LRUCache* cache, CacheNode* node) { if (!cache->head) { // 如果链表为空,则直接设置头尾指针 cache->head = cache->tail = node; } else { node->next = cache->head; cache->head->prev = node; cache->head = node; } } ``` #### 删除节点 从链表中移除某个节点: ```c void remove_node(CacheNode* node) { if (node->prev) { node->prev->next = node->next; } if (node->next) { node->next->prev = node->prev; } if (node == node->list->head) { node->list->head = node->next; } if (node == node->list->tail) { node->list->tail = node->prev; } } ``` #### 移动节点到头部 更新已有节点的位置至链表头部: ```c void move_to_head(LRUCache* cache, CacheNode* node) { if (cache->head != node) { remove_node(node); add_to_head(cache, node); } } ``` #### 获取缓存值 尝试从缓存中获取指定键对应的值,如果命中则调整该节点的位置;如果没有找到,则返回 -1: ```c int get_cache_value(LRUCache* cache, const char* key) { CacheNode* current = cache->head; while (current) { if (strcmp(current->key, key) == 0) { move_to_head(cache, current); current->access_count++; // 更新访问计数 current->last_access_time = time(NULL); // 更新最后访问时间 return current->value; } current = current->next; } return -1; // 键不存在于缓存中 } ``` #### 插入或替换缓存值 向缓存中插入新的键值对,或者覆盖已有的键值对。如果缓存容量达到上限,则按照 LRU-K 策略淘汰最不常使用的节点: ```c void set_cache_value(LRUCache* cache, const char* key, int value) { CacheNode* existing_node = cache->head; while (existing_node && strcmp(existing_node->key, key) != 0) { existing_node = existing_node->next; } if (existing_node) { // 已存在此键,更新其值 existing_node->value = value; move_to_head(cache, existing_node); existing_node->access_count++; existing_node->last_access_time = time(NULL); } else { // 新增键值对 if (cache->count >= CACHE_SIZE) { // 容量不足,需淘汰旧节点 CacheNode* lru_node = cache->tail; while (lru_node && lru_node->access_count > 1) { // 找到满足条件的 LRU-K 节点 lru_node = lru_node->prev; } if (lru_node) { remove_node(lru_node); free(lru_node); cache->count--; } } CacheNode* new_node = create_node(key, value); add_to_head(cache, new_node); cache->count++; } } ``` --- ### 总结 上述代码实现了基本的 LRU-K 缓存机制,通过维护 `access_count` 和 `last_access_time` 来跟踪每个缓存项的访问频率及时效性。这种设计能够有效应对复杂的访问模式,同时保持较高的运行效率[^4]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值