[leveldb] LRUCache

114 阅读6分钟

struct LRUHandle

是哈希表的一个节点类型及LRUCache中的链表节点类型

后面在LRUCache的LookUp中会使用

return reinterpret_cast<Cache::Handle*>(e)

强转为Handle这个空类对象,使用时再强转回LRUHandle,获取其void*类型的value(这个转换过程是为了啥,为什么不直接用LRUHandle?)

void* Value(Handle* handle) override {
    return reinterpret_cast<LRUHandle*>(handle)->value;
  }

结构定义:

struct LRUHandle {
  void* value;   // 这个value类型是TableAndFile
  void (*deleter)(const Slice&, void* value); // 由用户传入
  LRUHandle* next_hash;   // 哈希表解决冲突的链表
  LRUHandle* next;  // lru/in_use双向链表指针
  LRUHandle* prev;  // lru/in_use双向链表指针
  size_t charge;  // TODO(opt): Only allow uint32_t?每条缓存纪录容量,累加为lrucache的usage_
  size_t key_length; // key长度,与key_data一起构造key的slice
  bool in_cache;     // Whether entry is in the cache. Insert操作会触发写入in_cache=true,表明已经在哈希表中了
  uint32_t refs;     // References, including cache reference, if present.
  uint32_t hash;     // Hash of key(); used for fast sharding and comparisons
  char key_data[1];  // Beginning of key,key_length是key的长度,配合使用
  ......
}

这里value类型定义为value*,需要用户在使用时进行一下转换,转到存入时的类型,例如:

Table* table = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;

HandleTable

leveldb自己实现的哈希表,据称比g++内置hash表(随机读)快

解决冲突方式:开链法,尾插

成员

// 这个table是一些buckets的数组组成的,每个bucket是一个hash到bucket的缓存链表
  uint32_t length_;  // bucket数量,每个bucket中hash & (length_ - 1)
相同
  uint32_t elems_;   // key数量,等于length_时启动resize
  LRUHandle** list_;   // 指针数组,第一维为bucket,第二维是LRUHandle*类型指针,LRUHandle中的next_hash指向同一个slot中的下一个元素

插入:

  // 哈希表插入,老节点替换为新节点返回查找到的key对应的老的LRUHandle节点或者插入新节点返回nullptr
  // 如果不存在,那么从链表尾部插入后返回新节点,如果插入后表太大了就启动Resize
  LRUHandle* Insert(LRUHandle* h) {
    LRUHandle** ptr = FindPointer(h->key(), h->hash); // 返回hash和key都匹配的element,如果没有返回对应bucket的最后一个节点(nullptr)
    LRUHandle* old = *ptr;
    h->next_hash = (old == nullptr ? nullptr : old->next_hash);  // 
    *ptr = h;
    if (old == nullptr) {
      ++elems_;
      if (elems_ > length_) {    // 元素数量大于bucket数量,哈希表太大了,重新分配一下内存
        // Since each cache entry is fairly large, we aim for a small
        // average linked list length (<= 1).
        // 扩容到新的大小为不小于elems_的2的幂次
        Resize();
      }
    }
    return old;
  }

LRUCache

cache.cc

通过哈希表和链表实现的最近最少使用缓存

在这个类里维护了一个哈希表table_;两个环形双向链表,一个lru_,一个in_use_;双向链表指针为struct LRUHandle的成员prev和next

lru_为不常用的元素,容量不够用了会按写入顺序清空并删除其在哈希表中的对应元素

in_use_为常用的元素

class LRUCache {
  ......
 private:
  ......
  // Initialized before use.
  // 初始化决定,哈希表的最大容量,如果usage_大于它了,就删元素
  size_t capacity_; 

  // mutex_ protects the following state.
  mutable port::Mutex mutex_;
  size_t usage_ GUARDED_BY(mutex_);

  // 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_);  // LRU虚拟表头,保存最近没有在用的元素

  // 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_);    // in-use虚拟表头,保存最近使用的元素

  HandleTable table_ GUARDED_BY(mutex_);   // 哈希表 
};

什么时候元素放入lru_:用户调用Release会执行UnRef,如果发现引用次数降到1了e->in_cache && e->refs == 1,就从in_use_删除放到lru_

什么时候元素放入in_use_: 执行lookup操作ref++,如果在lru_里面,那么从lru_删除并放入in_use_;insert操作都会节点链接到in_use_链表

什么时候彻底删除节点:新元素插入,老的相同key元素会被删除;用户调用Release执行UnRef,如果发现没有引用了即e->refs == 0,直接在链表和哈希表删除该节点

插入函数实现:

// 使用key构造一个新节点插入哈希表并放入in_use_链表头部
// 可能会触发lru_链表的清除操作
Cache::Handle* LRUCache::Insert(const Slice& key, uint32_t hash, void* value,
                                size_t charge,
                                void (*deleter)(const Slice& key,
                                                void* value)) {
  MutexLock l(&mutex_);
  // new一个新节点并填充字段
  LRUHandle* e =
      reinterpret_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
  e->value = value;
  e->deleter = deleter;
  e->charge = charge;
  e->key_length = key.size();
  e->hash = hash;
  e->in_cache = false;
  e->refs = 1;  // for the returned handle. 返回的handle加一次ref
  std::memcpy(e->key_data, key.data(), key.size());

  // 节点放入in_use_,插入table_
  if (capacity_ > 0) {
    e->refs++;  // for the cache's reference.cache的handle加一次ref
    e->in_cache = true;
    LRU_Append(&in_use_, e); // e->next = in_use_
    usage_ += charge;
    FinishErase(table_.Insert(e));  //  输入:老节点在哈希表中的entry指针或者nullptr,这个老节点已经被新节点替代,更新操作需要回收老元素
  } else {  // don't cache. (capacity_==0 is supported and turns off caching.)
    // next is read by key() in an assert, so it must be initialized
    e->next = nullptr; 
  }
  // lru_非空,容量不够了,执行冷链lru_和hash表擦除操作直到容量不小于usage
  while (usage_ > capacity_ && lru_.next != &lru_) { 
    LRUHandle* old = lru_.next;
    assert(old->refs == 1);
    bool erased = FinishErase(table_.Remove(old->key(), old->hash));
    if (!erased) {  // to avoid unused variable when compiled NDEBUG
      assert(erased);
    }
  }

  return reinterpret_cast<Cache::Handle*>(e);
}

链表各种操作及引用计数:

// 如果在lru_,从lru_删除,添加到in_use_
// refs自增
void LRUCache::Ref(LRUHandle* e) {
  if (e->refs == 1 && e->in_cache) {  // If on lru_ list, move to in_use_ list.
    LRU_Remove(e);
    LRU_Append(&in_use_, e);
  }
  e->refs++;
}

// delete指针e(从in_use_删除放入lru_)
// 释放条件:refs自减后,1.refs为0,并且不在cache里面直接调用deleter删除
//                    2.refs为1,并且在cache里,修改节点的pre/next指针将节点从原来的链表删除,将节点放入lru_
void LRUCache::Unref(LRUHandle* e) {
  assert(e->refs > 0);
  e->refs--;
  if (e->refs == 0) {  // Deallocate.
    assert(!e->in_cache);
    (*e->deleter)(e->key(), e->value); // 调用delete析构e
    free(e);
  } else if (e->in_cache && e->refs == 1) {
    // No longer in use; move to lru_ list.
    LRU_Remove(e);
    LRU_Append(&lru_, e);
  }
}
// 从链表删除节点e
void LRUCache::LRU_Remove(LRUHandle* e) {
  e->next->prev = e->prev;
  e->prev->next = e->next;
}
// 把e节点添加到list链表尾部
void LRUCache::LRU_Append(LRUHandle* list, LRUHandle* e) {
  // Make "e" newest entry by inserting just before *list
  e->next = list;
  e->prev = list->prev;
  e->prev->next = e;
  e->next->prev = e;
}

可以看到,in_use_ 和lru_链表在插入元素的时候都是从尾部append上去的,而lru_删除元素是从头部开始删除的,保证了最近使用的晚删除

LookUp:

// 在table_里查找key,会触发从lru_表向in_use_表迁移
// 返回值:找到返回指针,找不到返回nullptr
//        找到,refs自增1;在lru_的话,移动到in_use_
Cache::Handle* LRUCache::Lookup(const Slice& key, uint32_t hash) {
  MutexLock l(&mutex_);
  LRUHandle* e = table_.Lookup(key, hash);
  if (e != nullptr) {
    Ref(e);
  }
  return reinterpret_cast<Cache::Handle*>(e);
}

Release:

// Lookup获取到Handle指针或Insert,使用完成后需要主动调用Release
// 将handle节点直接删除或者从in_use_删除放入lru_
void LRUCache::Release(Cache::Handle* handle) {
  MutexLock l(&mutex_);
  Unref(reinterpret_cast<LRUHandle*>(handle));
}

删除元素:

// e为旧纪录,这个函数处理元素删除操作,把e从链表删除,in_cache改为false,usage_减掉charge,删除节点e
bool LRUCache::FinishErase(LRUHandle* e) {
  if (e != nullptr) {
    assert(e->in_cache);
    LRU_Remove(e);  // 从next_/pre_这个链表删除e元素,这里不管它是在in_use_还是lru_
    e->in_cache = false; //这里in_cache为false了,所以在Unref里面不会放入lru
    usage_ -= e->charge;
    Unref(e);
  }
  return e != nullptr;
}

ShardedLRUCache

由于LRUCache的哈希表随着冲突增大,以及LookUp和Insert等函数加锁操作随着查找次数上升性能下降,将LRUCache封装为了分片实现,即ShardedLRUCache

ShardedLRUCache继承自Cache,分shard的LRU,每个shard是一个LRUCache

默认16个shard,每个分片容量为(capacity + (16 -1)) / 16,使用哈希算法将key分散到不同shard

成员:

  LRUCache shard_[kNumShards];  // 16个shard
  port::Mutex id_mutex_;  // last_id_的锁
  uint64_t last_id_;      // 没看到在哪用

ShardedLRUCache负责将每个key分配到不同的shard,这个过程是无锁的,分到不同shard之后LRUCache的操作有锁

TableCache

table_cache.h / table_cache.cc

leveldb使用的缓存,用于缓存ldb文件内容,先在LRU缓存查找(Lookup),找不到再读取lsb文件查找key,找到就insert到LRU中

void* value; // LRUHandle中这个value存取TableAndFile类型指针

成员变量:

  Env* const env_;
  const std::string dbname_;
  const Options& options_;
  Cache* cache_;    // Cache是一个基类,在构造函数中使用NewLRUCache初始化它