leveldb之cache

32 篇文章 1 订阅

当向leveldb写入数据时,首先是将数据写入leveldb的Memtable(Memtable可能转化为IMMemtable)中,Memtable是存储在内存中的。只有经过compaction操作后,才会将内存中的数据写入到磁盘中的sstable中。 
当要读数据时,首先在Memtable中查找,若没有找到,则在sstable中继续查找。而sstable是存储在磁盘中的,这样就需要进行多次磁盘操作,速度会非常慢。为了加快查找速度,leveldb在采用了Cache的方式,尽最大可能减少随机读操作。

cache分为Table Cache和 Block Cache两种,其中Table Cache中缓存的是sstable的索引数据,Block Cache缓存的是Block数据,Block Cache是可选的,即可以在配置中来选择是否打开这个功能。

当要进行Compaction操作调用CompactMemTable()时,会调用WriteLevel0Table(),此时则会创建一个Meta File,并保存在Table Cache中,然后可通过Table Cache进行读取。

leveldb中的Cache主要用到了双向链表、哈希表和LRU(least recently used)思想。

1、LRUHandle

LRUHandle表示了Cache中的每一个元素,通过指针形成一个双向循环链表:

struct LRUHandle {
  void* value;//cache存储的数据
  void (*deleter)(const Slice&, void* value);//是将数据从Cache中清除时执行的函数
  LRUHandle* next_hash;//解决hash碰撞,指向下一个hash值相同的节点
  LRUHandle* next;//next和prev构成双向循环链表
  LRUHandle* prev;
  size_t charge;      // 所占内存大小
  size_t key_length;
  uint32_t refs;
  uint32_t hash;      // Hash of key(); used for fast sharding and comparisons
  char key_data[1];   // Beginning of key

  Slice key() const {
    if (next == this) {
      return *(reinterpret_cast<Slice*>(value));
    } else {
      return Slice(key_data, key_length);
    }
  }
};
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在Table Cache中,Cache的key值是SSTable的文件名称,Value部分包含两部分,一个是指向磁盘打开的SSTable文件的文件指针,这是为了方便读取内容;另外一个是指向内存中这个SSTable文件对应的Table结构指针。这样就将不同的sstable文件像cache一样进行管理。

leveldb通过LRUHandle 结构将hash值相同的所有元素串联成一个双向循环链表,通过指针next_hash来解决hash 碰撞。

2、HandleTable

leveldb通过HandleTable维护一个哈希表:

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

  LRUHandle* Lookup(const Slice& key, uint32_t hash);
  LRUHandle* Insert(LRUHandle* h);
  LRUHandle* Remove(const Slice& key, uint32_t hash);
 private:
  uint32_t length_;
  uint32_t elems_;
  LRUHandle** list_;

  LRUHandle** FindPointer(const Slice& key, uint32_t hash) ;
  void Resize() ;
};
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
HandleTable只有3个成员变量: uint32_t length_;   //hash表长,相当于二维数组的元素个数
                          uint32_t elems_;   //hash表中元素总个数
                        LRUHandle** list_; //hash表头,相当于一个双向循环链表数组,其中每一个元素指向一个循环链表

初始时,length_=elems_=0, list_=NULL;此时hash表为空 
然后调用resize()为hash表分配空间,初始时分配4个元素,当空间不足时,则成倍增加。此时hash表的结构如下:

这里写图片描述

3、LRUCache

LRUCache顾名思义是指一个缓存,同时它用到了LRU的思想

class LRUCache {
 public:
  LRUCache();
  ~LRUCache();

  void SetCapacity(size_t capacity) { capacity_ = capacity; }
  Cache::Handle* Insert(const Slice& key, uint32_t hash,
                        void* value, size_t charge,
                        void (*deleter)(const Slice& key, void* value));
  Cache::Handle* Lookup(const Slice& key, uint32_t hash);
  void Release(Cache::Handle* handle);
  void Erase(const Slice& key, uint32_t hash);
 private:
  void LRU_Remove(LRUHandle* e);
  void LRU_Append(LRUHandle* e);
  void Unref(LRUHandle* e);

  // Initialized before use.
  size_t capacity_;

  // mutex_ protects the following state.
  port::Mutex mutex_;
  size_t usage_;

  LRUHandle lru_;
  HandleTable table_;
};
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

LRUCache维护了一个双向循环链表lru_和一个hash表table,当要插入一个元素时,首先将其插入到链表lru的尾部,然后根据hash值将其插入到hash表中。

当hash表中已存在hash值与要插入元素的hash值相同的元素时,将原有元素从链表中移除,这样就可以保证最近使用的元素在链表的最尾部,这也意味着最近最少使用的元素在链表的头部,这样即可实现LRU的思想。

capacity_:当前Cache的最大容量 
usage_:当前Cache已使用的内存大小,当超过最大容量时,从链表的头部开始移除元素 
mutex_:保证每个Cache都是线程安全的

4、Cache和ShardedLRUCache

Class Cache采用虚函数定义了Cache的接口:

class Cache {
 public:
  Cache() { }
  virtual ~Cache();
  struct Handle { };//handle corresponds to a mapping

  virtual Handle* Insert(const Slice& key, void* value, size_t charge,
                         void (*deleter)(const Slice& key, void* value)) = 0;
  virtual Handle* Lookup(const Slice& key) = 0;
  virtual void Release(Handle* handle) = 0;
  virtual void* Value(Handle* handle) = 0;//handle returned by Lookup()
  virtual void Erase(const Slice& key) = 0;

  virtual uint64_t NewId() = 0;
 private:
  void LRU_Remove(Handle* e);
  void LRU_Append(Handle* e);
  void Unref(Handle* e);

  struct Rep;
  Rep* rep_;
};
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

具体实现的ShardedLRUCache继承自Cache,实现了相应的功能:

class ShardedLRUCache : public Cache {
 private:
  LRUCache shard_[kNumShards];  //kNumShards = 2^4 = 16
  port::Mutex id_mutex_;
  uint64_t last_id_;
 public:
  virtual Handle* Insert(const Slice& key, void* value, size_t charge,
                         void (*deleter)(const Slice& key, void* value));
  virtual Handle* Lookup(const Slice& key);
  virtual void Release(Handle* handle) ;
  virtual void Erase(const Slice& key) ;
  virtual void* Value(Handle* handle) ;
};
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

由于每一个LRUCache是线程安全的,为了多线程访问,尽可能快速,减少锁开销,ShardedLRUCache内部将所有Cache根据hash值的高4位分为16份,即有16个LRUCache分片。

查找Key时首先计算key属于哪一个分片,分片的计算方法是取32位hash值的高4位,找到对应的LRUCache分配,然后在相应的LRUCache中进行查找,这样就大大减少了多线程的访问锁的开销。以Insert为例:

  virtual Handle* Insert(const Slice& key, void* value, size_t charge,
                         void (*deleter)(const Slice& key, void* value)) {
    const uint32_t hash = HashSlice(key);//首先得到hash值的高4位,用于找到相应的分片
    return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);//然后在对应的LRUCache中进行相应的插入、移除等操作
  }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

由上可知,ShardedLRUCache最终都是调用LRUCache的相应操作来实现的,因此下面将详细介绍LRUCache中的相关操作

5、LRUCache的具体操作

假设此时刚刚完成LRUCache的初始化和Resize()

插入元素:

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; //首先填充结构体LRUHandle
  e->deleter = deleter;
  e->charge = charge;
  e->key_length = key.size();
  e->hash = hash;
  e->refs = 2;  // One from LRUCache, one for the returned handle
  memcpy(e->key_data, key.data(), key.size());
  LRU_Append(e); //然后将元素加入到双向循环链表中
  usage_ += charge;

  LRUHandle* old = table_.Insert(e); //将元素加入到hash表中
  if (old != NULL) {//当hash表中已存在hash值相同的元素时,将原有的元素移除
    LRU_Remove(old);
    Unref(old);
  }

 //当所有占用空间超过最大容量时,则从双向链表的表头开始移除元素
 //由于表头元素是最早加入的,因此是实现了LRU思想
  while (usage_ > capacity_ && lru_.next != &lru_) {
    LRUHandle* old = lru_.next;
    LRU_Remove(old);
    table_.Remove(old->key(), old->hash);
    Unref(old);
  }
  return reinterpret_cast<Cache::Handle*>(e);
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

1) 调用LRU_Append(e) 将元素加入到双向循环链表中:

双向链表的插入操作示意图如下: 
这里写图片描述

当插入节点D时:

void LRUCache::LRU_Append(LRUHandle* e) {
  // Make "e" newest entry by inserting just before lru_
  e->next = &lru_;//D->next 指向表头
  e->prev = lru_.prev; //D->prev=A->prev=C
  e->prev->next = e; //D->prev->next = C->next = D
  e->next->prev = e; //D->next->prev = A->prev = D
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这样就将一个节点插入到双向链表的尾部了

2) 调用 LRUHandle* old = table_.Insert(e); 向hash表中插入一个元素:

  LRUHandle* Insert(LRUHandle* h) {
    LRUHandle** ptr = FindPointer(h->key(), h->hash);//首先在hash表中进行查找
    LRUHandle* old = *ptr;
    h->next_hash = (old == NULL ? NULL : old->next_hash);//当old不为NULL时,表示hash表中已存在一个与要插入的元素的hash值完全相同的元素,这样只是替换元素,不会改变元素个数
    *ptr = h;
    if (old == NULL) {//old为空表示插入一个新元素,要增加元素个数并调整hash表大小
      ++elems_;
      if (elems_ > length_) {
        Resize();
      }
    }
    return old;
  }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

当要插入一个元素时,首先进行查找:

  LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
    LRUHandle** ptr = &list_[hash & (length_ - 1)];
    while (*ptr != NULL &&
           ((*ptr)->hash != hash || key != (*ptr)->key())) {
      ptr = &(*ptr)->next_hash;
    }
    return ptr;
  }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

假设首先要插入元素的hash值为1: 
此时length_ = 4,则 [ hash & (length_ - 1) ]=[01 & 11]= [1],即ptr= &list_[1],此时*ptr = NULL 
则返回ptr并完成插入操作,此时hash表为:

这里写图片描述

当要继续插入元素,且元素的hash值为5时: 
此时length_ = 4,则 [ hash & (length_ - 1) ]=[101 & 11]= [1],ptr= &list_[1],此时*ptr = e(1),继续向后查找,ptr=&(*ptr)->next_hash; 最终*ptr = NULL ,则返回ptr并完成插入操作,此时hash表为:

这里写图片描述

插入操作的基本操作为:首先构造LRUHandle结构体,然后将其插入到双向链表中,然后根据hash值将其插入到hash表中,并调整hash表的大小,最后根据内存大小,移除双向链表表头后第一个元素,来实现LRU算法

其它操作如Lookup()、Erase()、Release()等,都是先根据hash值的高四位来找到对应的LRUCache,然后在相应的hash表中根据hash值找到相应元素,最终来对找到的元素执行相关操作。

6、总结

1)一个SharedLRUCache根据hash值的高4位,将所有的Cache分为16份,每一份为一个LRUCache。 
2)LRUCache用来维护所有元素(LRUHandle),通过锁mutex来保证线程安全,同时记录当前 
Cache的最大容量和已用内存。 
3)每一个LRUCache中有一个循环链表(LRUHandle)和一个hash表(HandleTable),通过循环链 
表实现LRU思想,通过hash表来加快查找速度。

假定设置的最大容量为4,即capacity_=4,然后依次向其中插入元素1、5、2、4、9,简单的示意图如下:

当插入元素1,5, 2, 4后,usage=4。每个元素在hash表中的位置为 list [ hash&length_-1 ] ,由此可知 ,1和5,2和4有着相同的hash值,分别对应list[1],list[2] 
此时得到如下结果(为简化,与循环链表头部相连的部分指针线未画出):

这里写图片描述

此时已达到最大容量,假设要继续插入一个元素9(对应list[1]),插入9后,usage>capacity,则要从链表头部开始移除元素,即元素1会被移除,得到结果如下:

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值