ninja_ken  

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

其中nextprev分别对应着该元素所在的环形双向链表前、后一个元素(再次强调一次,任何一个元素只会存在两个链表中的一个)。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,提高查询效率。

posted on 2022-06-07 15:44  ninja_ken  阅读(257)  评论(0)    收藏  举报