【LevelDB源码阅读】Cache

是什么

leveldb内部实现的缓存

为什么要用

  • 性能高于内嵌哈希表

学到什么

  • 与(&)操作进行模运算
  • 减少cache锁操作,可以分为多组cache

源码分析

LRUHandle

缓存中代表键值对的数据结构

// An entry is a variable length heap-allocated structure.  Entries
// are kept in a circular doubly linked list ordered by access time.
struct LRUHandle {
  void *value;  // 存储键值对的值,可以为任意类型
  void (*deleter)(const Slice &, void *value);  // 函数指针当前结构体的清除函数
  LRUHandle *next_hash;                         // 用户hash冲突使用
  LRUHandle *next;                              // 当前节点下一个节点
  LRUHandle *prev;                              // 当前节点上一个节点
  size_t charge;                                // 当前节点占用内存
  size_t key_length;                            // 当前节点键的长度
  bool in_cache;     // Whether entry is in the cache.
  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

  Slice key() const {
    // next_ is only equal to this if the LRU handle is the list head of an
    // empty list. List heads never have meaningful keys.
    assert(next != this);

    return Slice(key_data, key_length);
  }
};

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_;  // hash表桶数组长度
  uint32_t elems_;   // 哈希表存储元素的数量
  LRUHandle **list_;  // 哈希数组指针,其中每个数组元素存储指针类型

构造函数和析构函数

HandleTable() : length_(0), elems_(0), list_(nullptr) { Resize(); }
  ~HandleTable() { delete[] list_; }

主要接口函数

  • 查询
LRUHandle* Lookup(const Slice &key, uint32_t hash) {
    return *FindPointer(key, hash);
  }

其中FindPointer方法实现如下,首先确定在哈希数组的哪个哈希桶中,然后沿着哈希链表查找。
如果某个LRUHandle* 存在相同hash和key值,则返回LRUHandle*的二级指针,即上一个LRUHandle*的next_hash的二级指针。
如果不存在,则返回bucket中最后一个 LRUHandle*的next_hash,即为nullptr。

// 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)];  // 下标结果是0到length-1
    while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) {
      ptr = &(*ptr)->next_hash;  // 迭代哈希链找到键值相等节点,ptr为上一节点next_hash值
    }
    return ptr;
  }
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;  //将ptr值也就是对应上一个LRUHandle的next_hash值赋值为h来跳过old
    if (old == nullptr) {  // h是新节点
      ++elems_;
      // Since each cache entry is fairly large, we aim for a small
      // average linked list length (<= 1).
      if (elems_ > length_) {
        Resize();
      }
    }
    return old;
  }

其中Resize方法实现如下:

  1. 初始大小为4,每次以原有大小的2倍扩容
  2. 以新的长度创建哈希桶大小,并设置每个哈希桶为空
  3. 遍历原有每个哈希桶的每个元素,重新确定在新哈希桶的位置,采用头插法插入到新哈希桶的每个链表中
  4. delete原有哈希链表,并赋值为新的哈希链表
void Resize() {
    uint32_t new_length = 4;
    while (new_length < elems_) {
      new_length *= 2;  // 每次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;  // 一级指针上二级指针通过next_hash连接
        uint32_t hash = h->hash;
        LRUHandle **ptr = &new_list[hash & (new_length - 1)];  // 确定在新桶中位置
        h->next_hash = *ptr;  // 第一次*ptr为nullptr,其它为上个循环h的地址
        *ptr = h;
        h = next;
        ++count;
      }
    }
    assert(elems_ == count);
    delete[] list_;
    list_ = new_list;
    length_ = new_length;
  }
  • 删除
LRUHandle* Remove(const Slice &key, uint32_t hash) {
    LRUHandle **ptr = FindPointer(key, hash);
    LRUHandle *result = *ptr;
    if (result != nullptr) {  // 找到节点
      *ptr = result->next_hash;
      --elems_;
    }
    return result;
  }

LRUCache

数据成员如下包括:

  • cache总大小
  • cache已经使用大小
  • 冷链表(lru_),即在cache中,但没有被外部引用,cache容量满时会被淘汰
  • 热链表(in_use_),即在cache中,同时又被外部引用,cache容量满时也不会被淘汰
  • 哈希表(table_),用于快速查找(链表结构则用于快速插入和删除)
// Initialized before use.
  size_t capacity_;  // cache总容量

  // mutex_ protects the following state.
  mutable port::Mutex mutex_;
  size_t usage_ GUARDED_BY(mutex_);  // 已经使用的cache空间

  // 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_);  // 二级指针数组,用于缓存数据

构造函数和析构函数
构造函数,初始化所有数据成员,其中哈希执行默认初始化。

LRUCache::LRUCache() : capacity_(0), usage_(0) {
  // Make empty circular linked lists.
  lru_.next = &lru_;
  lru_.prev = &lru_;
  in_use_.next = &in_use_;
  in_use_.prev = &in_use_;
}

执行析构函数时,热链表中不应该有元素,函数体主要删除冷链表中元素。

LRUCache::~LRUCache() {
  assert(in_use_.next == &in_use_);  // Error if caller has an unreleased handle
  for (LRUHandle *e = lru_.next; e != &lru_;) {
    LRUHandle *next = e->next;
    assert(e->in_cache);
    e->in_cache = false;
    assert(e->refs == 1);  // Invariant of lru_ list.
    Unref(e);
    e = next;
  }
}

主要对外接口

  • 查找

根据key和hash值在哈希表中进行查找,查找到后更新热链表。

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);
}
  • 插入

首先申请LRUHandle内存并且初始化,LRUHandle对象指针会返回给调用方,所以refs此时为1,然后添加到in_use_链表尾部,refs此时为2,同时更新in_cache=true和cache使用量。如果
插入到哈希表table_中已经存在,则会将原有节点从cache中删除。最后如果容量超限,从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_);

  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.
  std::memcpy(e->key_data, key.data(), key.size());

  if (capacity_ > 0) {
    e->refs++;  // for the cache‘s reference.
    e->in_cache = true;
    LRU_Append(&in_use_, e);
    usage_ += charge;
    FinishErase(table_.Insert(e));  // 插入到哈希表中同时清理旧节点
  } 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;
  }
  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);
}
  • 删除

从哈希表中删除,然后从cache链表删除。

void LRUCache::Erase(const Slice& key, uint32_t hash) {
  MutexLock l(&mutex_);
  FinishErase(table_.Remove(key, hash));
}
  • 其它辅助方法

Ref方法表示要使用该cache,如果元素位于冷链表中,需要移到热链表。

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++;
}

Unref正好相反,表示用户不再使用该元素,需要将引用计数--,如果彻底没有用户使用,即引用计数为0,则删除这个元素,如果引用计数为1,则从热链表移到冷链表中。

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);
    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);
  }
}

其中从链表删除元素和将元素插入指定链表如下:

void LRUCache::LRU_Remove(LRUHandle* e) {
  e->next->prev = e->prev;
  e->prev->next = e->next;
}

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;
}

SharedLRUCache

为什么有SharedLRUCache?
LRUCache的接口都会加锁,为了更少的锁持有时间以及更高的缓存命中率,可以定义多个LRUCache,分别处理不同hash取模后的缓存处理。SharedLRUCache是LRUCache上的一层封装,管理16个cache,对外提供服务,外部调用接口都委托内部某个LRUCache。

数据成员

LRUCache shard_[kNumShards];  // 16个LRUCache

SharedLRUCache的主要作用就是计算Hash值,选择LRUCache,代码如下:

static inline uint32_t HashSlice(const Slice &s) {
    return Hash(s.data(), s.size(), 0);
  }

  static uint32_t Shard(uint32_t hash) {  // 得到shard_数组下标
    return hash >> (32 - kNumShardBits);  // 取hash高4位得到shard_数组下标
  }

主要对外接口
这些接口都是先计算hash值,然后根据hash值选择LRUCache,最后通过调用LRUCache来实现。

  • 插入
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);
  }
  • 查找
Handle* Lookup(const Slice &key) override {
    const uint32_t hash = HashSlice(key);
    return shard_[Shard(hash)].Lookup(key, hash);
  }
  • 删除
void Erase(const Slice &key) override {
    const uint32_t hash = HashSlice(key);
    shard_[Shard(hash)].Erase(key, hash);
  }
  • 查看缓存使用大小
size_t TotalCharge() const override {
    size_t total = 0;
    for (int s = 0; s < kNumShards; s++) {
      total += shard_[s].TotalCharge();
    }
    return total;
  }