leveldb源码剖析2.3–核心设计之文件缓存
引言
在leveldb中对文件数据缓存分为TableCache和Options中的block_cache,两个缓存的实现都是基于ShardedLRUCache类。
1 设计原理
1.1 核心思想
LRUCache主要缓存KV数据,其核心思想如下:
- 首先,将Key通过哈希算法散列。然后通过哈希链表存储数据;为了解决多线程并发互斥加锁,所以分为16个隔离的桶去实现哈希链表的存储;
- 最后,为了在当缓存达到上限删除最久未用的数据,采用链表将未使用的数据串列起来。当缓存超出时,从链表头剔除陈旧数据。
1.2 数据结构
整个数据节点LRUHandle分为三个维度,即HandleTable、LRUCache中的lru_和in_use_。如下图:
其中HandleTable是哈希表的实现;lru_是对未被外部引用的双向节点链表头;in_use_是正被外部引用的双向节点链表头。特别地,lru_和in_use_是HandleTable类型。
2 源码解读
2.1 核心代码
整个Cache的核心逻辑如下:
- ShardedLRUCache实现Cahce接口,对外提供功能;每个ShardedLRUCache聚合16个LRUCache
- 每个LRUCache聚合一个HandleTable,每个加入的LRUHandle节点以lru_和in_use_被串成链表;
- 每个加入的LRUHandle节点也会以哈希链表形式聚合在HandleTable之中。从而便于快速查询和插入。
2.2 相关缓存
在leveldb中,主要有如下Cache对象:
- TableCache
- block_cache
2.2.1 TableCache
TableCache中key为文件序号,value为如下结构体:
struct TableAndFile {
RandomAccessFile* file; //sstable文件操作指针
Table* table; //访问sstable数据的操作对象
};
2.2.2 block_cache
block_cache中key为如下序列化结构:
block_cache中value为指向Block对象的指针。
2.3 可优化项
- HashTable的Insert函数
LRUHandle* Insert(LRUHandle* h) {
LRUHandle** ptr = FindPointer(h->key(), h->hash);
LRUHandle* old = *ptr;
h->next_hash = (old == nullptr ? nullptr : old->next_hash);
*ptr = h;
if (old == nullptr) {
++elems_;
if (elems_ > length_) {
// 重新扩容哈希表
Resize();
}
}
return old;
}
- HashTable的Resize函数
void Resize() {
uint32_t new_length = 4;
while (new_length < elems_) {
new_length *= 2;
}
LRUHandle** new_list = new LRUHandle*[new_length];
memset(new_list, 0, sizeof(new_list[0]) * new_length);
uint32_t count = 0;
for (uint32_t i = 0; i < length_; i++) {
LRUHandle* h = list_[i];
//反转链表到新的哈希表中
while (h != nullptr) {
LRUHandle* next = h->next_hash;
uint32_t hash = h->hash;
LRUHandle** ptr = &new_list[hash & (new_length - 1)];
h->next_hash = *ptr;
*ptr = h;
h = next;
count++;
}
}
assert(elems_ == count);
delete[] list_;
list_ = new_list;
length_ = new_length;
}
问题:
通过上图的代码发现,在HashTable扩容时需要遍历存储已加入的LRUHandle到新扩容的哈希表
优化:
可以将list_的数组结构改为红黑树存储,用红黑树实现哈希表功能。