LevelDB的缓存机制

缓存的作用

leveldb为了提高写的性能,牺牲了部分的读性能。最差的情况可能需要遍历各个level中的每个文件。为了缓解读性能,leveldb引入了缓存机制,当然,版本信息中包含各个level的文件元信息在一定程度上也可以提高读性能。

基本组件

整个缓存模块的基本组件由上至下分为:ShardedLRUCacheLRUCacheHandleTableLRUHandle

  • ShardedLRUCache:这个是为了减少频繁加锁解锁开销的所设计的,采用分区的思想,把不同的元素划分到不同的LRUCache中。
  • LRUCache:这个分为两部分:哈希表和双向循环链表,用于管理缓存数据的结构
  • HandleTable:哈希表,采用开链法的思想避免hash冲突,里面存放一个个具体的元素
  • LRUHandle:表示元素的实体,<key,value>结构

其四个的大致关系如下图:
在这里插入图片描述

LRUCache

这个结构用于存储<key,value>结构的数据。其在设计上用途特别多,next_hash用于hash表;prevnext则是用于双向循环链表。而且其还采用了和shared_ptr相同的设计,只有当这个LRUCache的引用计数为0时才会真正的去调用用户传入的deleter去释放资源。
在这里插入图片描述

  void* value;  //<key,value>
                //中的value值,通过指针访问,可以是任何类型,这个可以替换成 tem
  void (*deleter)(
      const Slice&,
      void*
          value);  //自定义的删除器,因为元素可能是在堆内存的申请的,需要特殊的释放
  LRUHandle* next_hash;  //解决hash碰撞,指向下一个hash值相同的元素
  LRUHandle* next;       //用于list 构成双向循环链表
  LRUHandle* prev;       //用于list 构成双向循环链表
  size_t charge;         // 所占内存大小
  size_t key_length;     // key的长度
  bool in_cache;         // 元素是否在LRU Cache中
  uint32_t refs;         // 元素的引用计数,主要用于Remove函数
  uint32_t
      hash;  // 通过离线计算的思想,计算元素的Hash值,在比较的时候直接比较这个Hash值,不用进行字符串比较那些费时的操作
  char key_data
      [1];  // Key的开始,多个Key在相同的内存空间,采用key_begin+key_length的方式来定位Key

HashHandle

这个类会构建一个hash表来存储实际的LRUHandle,其属性只有三个:

 private:
  uint32_t length_;   // hash表长
  uint32_t elems_;    // hash表元素的个数
  LRUHandle** list_;  // hash表,每个表项都是一个双向循环数组

我们可以看到,LevelDB使用的一个二级指针来表示一个一个Hash表,这个是我认为比较有新意的点。我们先看一下它表现出来的图示:
在这里插入图片描述


LevelDB中hash桶默认的个数就是4个

插入元素

在hash表中插入元素首先要做的就是知道这个值应该被我们放到哪个hash桶里面,这个过程对应的就是函数FindPointer

  /**
   * @brief 找到key所应该插入的双向循环链表
   *
   * @param key key值
   * @param hash key的hash值
   * @return LRUHandle** 适合插入key的位置
   */
  LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
    // length-1 相当于对hash进行取模的hash运算
    LRUHandle** ptr = &list_[hash & (length_ - 1)];

    //因为有hash冲突问题,所以这里我们拿到指针不一定是想要的,需要不断对比
    while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) {
      ptr = &(*ptr)->next_hash;
    }
    return ptr;
  }

而找到元素应该插入的位置之后便直接利用头插法插入元素就可以。假设我们现在要插入key为10,此时我们的length为4,那么011 & 1010 = 10,我们就用头插法把它插入第二个桶之中就可以:
在这里插入图片描述
而插入之中,我们难免会遇到空间不够的情况,这个时候我们就要进行扩容。LevelDB的扩容规则就是整个hash表的元素个数>桶的个数,所以说上面的表咱画的有点不正规了,哈哈哈哈。

LevelDB会执行二倍扩容的策略,比如说咱当前的6个元素是大于4个桶的个数的,那么下面我们就要拿到8个桶的hash表了。之后对里面的元素进行rehash过程,重新构建hash表并删除原有hash表。
在这里插入图片描述
整个过程的代码如下:

  /**
   * @brief 哈希表中插入元素
   *
   * @param h 待插入元素的指针
   * @retval LRUHandle* 原有元素存在于hash表中就返回指向原有元素的指针
   * @retval nullptr 不存在返回
   */
  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_) {
        // Since each cache entry is fairly large, we aim for a small
        // average linked list length (<= 1).
        // 扩容
        Resize();
      }
    }
    return old;
  }

LRUCache

LRUCache采用LRU的缓存结构,其主要结构有两个LRU ListHash Table。其中特别要注意的是其设计的两个链表lruin_use,同一个元素是不同同时存在于这两个链表。
在这里插入图片描述

 private:
  size_t capacity_;                  // LRU 缓存的总共容量,字节数
  mutable port::Mutex mutex_;        //锁
  size_t usage_ GUARDED_BY(mutex_);  // LRU 缓存使用的容量
  LRUHandle lru_ GUARDED_BY(
      mutex_);  // LRU List, prev指向的是比较新的数据,next指向比较旧的数据
                //这里面的数据满足条件refs==1 && in_cache==true
  LRUHandle in_use_
      GUARDED_BY(mutex_);  // in_use list,里面存放着被client使用的数据
                           //数据满足条件refs >= 2 and in_cache==true
  HandleTable table_ GUARDED_BY(mutex_);  // HashHandle,哈希表

插入元素

而对于这缓存的插入元素操作来说,其首先会根据上层传递过来的数据,申请LRUHandle所需的内存空间并构建这个元素。同时,这里会初始的把引用计数设置为1,因为这个数据传过来就是要被外部使用的。之后,把它插入到in_use链表和hash表中,这里要注意的就是如果hash表之前有这个元素,会把老元素通过FinishErase删除,同时将新元素添加至hash表。
在这里插入图片描述
而LRUCache则没有像HashTable那样的扩容机制,所以一旦超了界限其就会删除最旧的数据。

/**
 * @brief 元素插入到缓存中
 *
 * @param key key
 * @param hash 元素key的hash值
 * @param value 元素value
 * @param charge 元素所占的内存大小
 * @param deleter 自定义的删除器
 * @return Cache::Handle* 返回指向元素的指针
 */
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_);

  // 内存构建缓存元素
  //这里-1时因为 char key[1];
  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. 这个被cache引用
    e->in_cache = true;
    LRU_Append(&in_use_, e);        //加入in_use list
    usage_ += charge;               //缓存使用的字节数++
    FinishErase(table_.Insert(e));  //元素添加至hash表
  } else {
    //缓存没有容量了,这个时候就单独返回e
    e->next = nullptr;
  }

  //容量不够,且lru 链表不为空
  //这里就是在清理lru list 直到满足容量
  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);
}

ShardedLRUCache

前面也说过,ShardedLRUCache就是为了避免锁开销而采用的一种基于分区的机制。这一点我们也可以从其的Insert函数看出来。其根据key计算出其应该在的LRUCache,之后调用这个LRUCacheInsert操作就可以了。

  Handle* Insert(const Slice& key, void* value, size_t charge,
                 void (*deleter)(const Slice& key, void* value)) override {
    //计算key的hash值
    const uint32_t hash = HashSlice(key);
    //Shared 是一个hash函数
    return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
  }

在这里插入图片描述

参考文献

[1] leveldb之cache
[2] levelDB 源码

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shenmingik

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值