LRU(Least Recently Used)是一种常见的cache策略,下面我们看一看LevelDB中LRU cache的设计与实现。
ShardedLRUCache
LevelDB中提供了一个创建Cache的函数,Cache* NewLRUCache(size_t capacity),其中参数capacity当然就是cache的大小,可以简单理解为字节数。函数的返回类型Cache是一个虚基类,其具体实现是子类ShardedLRUCache。该类是LRUCache类的sharded(分片)版本,也就是说SharedLRUCache内部有多个LRUCache分片,每次在插入一个KV对之前,先对key进行哈希,根据哈希结果落入哪个分片,再调用对应的LRUCache来完成KV对插入。具体代码如下
class ShardedLRUCache : public Cache {
...
Handle* Insert(const Slice& key, void* value, size_t charge,
void (*deleter)(const Slice& key, void* value)) override {
const uint32_t hash = HashSlice(key);
return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
}
其中,key是字符切片(Slice)类型,value是一个内存指针,charge是该KV对的大小,deleter是KV不再被使用的时候(引用计数refs == 0),自动调用的KV对清理函数。
LRUCache的设计与实现
LRUCache类中有三个关键成员,即lru_, in_use_和table_,代码如下
// A single shard of sharded cache.
class LRUCache {
...
// Dummy head of LRU list.
// lru.prev is newest entry, lru.next is oldest entry.
// Entries have refs==1 and in_cache==true.
LRUHandle lru_ GUARDED_BY(mutex_);
// Dummy head of in-use list.
// Entries are in use by clients, and have refs >= 2 and in_cache==true.
LRUHandle in_use_ GUARDED_BY(mutex_);
HandleTable table_ GUARDED_BY(mutex_);
lru_和in_use_都是LRUHandle类型,他们各自分别代表着一个环形双向链表的头结点(没错,链表这种数据结构非常适合来实现LRU,直观地看,链表越靠前的位置存放着越新插入的元素,链表越靠后则存放着越旧的元素)。LRU中的任一个元素,只会出现在这两个链表中的一个里,不会同时出现。LRUHandle的定义如下:
struct LRUHandle {
void* value;
void (*deleter)(const Slice&, void* value);
LRUHandle* next_hash;
LRUHandle* next;
LRUHandle* prev;
...
char key_data[1]; // Beginning of key
其中next和prev分别对应着该元素所在的环形双向链表前、后一个元素(再次强调一次,任何一个元素只会存在两个链表中的一个)。key_data字段利用了C语言中常见的struct末尾字符数组的技巧,比如在新创建一个链表节点LRUHandle元素时,可以这么写
LRUHandle* handle = malloc(sizeof(LRUHandle) - 1 + key.size())
这样其实相当于为key_data字段分配了key.size()个字节,正好可以存得下key。
下面再看一下class LRUCache中的table_字段,该字段里存放着所有的LRU元素,用于快速查找定位LRU中的元素(LRU链表查询效率较低)。如何能加速查找呢?table_是类HandleTable的实例,这个类的实际效果可以理解为对key+key hash值做了个映射表。这一点可以从它的这个成员函数中看出来:
class HandleTable {
...
// The table consists of an array of buckets where each bucket is
// a linked list of cache entries that hash into the bucket.
uint32_t length_;
uint32_t elems_;
LRUHandle** list_;
...
// Return a pointer to slot that points to a cache entry that
// matches key/hash. If there is no such cache entry, return a
// pointer to the trailing slot in the corresponding linked list.
LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
LRUHandle** ptr = &list_[hash & (length_ - 1)];
while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) { // why key != (*ptr)->key() here?
ptr = &(*ptr)->next_hash;
}
return ptr;
}
成员list_是一个数组,最大长度为length_,数组里每个元素都是一个LRUHandle环形双向链表的头节点。双向链表节点与对应list_的数组下标需满足hash & (length_ - 1) = 数组下标。注意看FindPointer函数里,寻找下一个元素时用的是next_hash字段。每当HandleTable中的元素数量达到数组的最大程度时,就会触发HandleTable::Resize()操作,让数组的最大长度 * 2,目的是为了尽量达到list_中每个元素——LRUHandle环形双向链表的链表元素数量为1,提高查询效率。

浙公网安备 33010602011771号