当向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)思想。
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);
}
}
};
在Table Cache中,Cache的key值是SSTable的文件名称,Value部分包含两部分,一个是指向磁盘打开的SSTable文件的文件指针,这是为了方便读取内容;另外一个是指向内存中这个SSTable文件对应的Table结构指针。这样就将不同的sstable文件像cache一样进行管理。
leveldb通过LRUHandle 结构将hash值相同的所有元素串联成一个双向循环链表,通过指针next_hash来解决hash 碰撞。
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() ; };
HandleTable只有3个成员变量: uint32_t length_; //hash表长,相当于二维数组的元素个数
uint32_t elems_; //hash表中元素总个数
LRUHandle** list_; //hash表头,相当于一个双向循环链表数组,其中每一个元素指向一个循环链表
初始时,length_=elems_=0, list_=NULL;此时hash表为空
然后调用resize()为hash表分配空间,初始时分配4个元素,当空间不足时,则成倍增加。此时hash表的结构如下:
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_;
};
LRUCache维护了一个双向循环链表lru_和一个hash表table,当要插入一个元素时,首先将其插入到链表lru的尾部,然后根据hash值将其插入到hash表中。
当hash表中已存在hash值与要插入元素的hash值相同的元素时,将原有元素从链表中移除,这样就可以保证最近使用的元素在链表的最尾部,这也意味着最近最少使用的元素在链表的头部,这样即可实现LRU的思想。
capacity_:当前Cache的最大容量
usage_:当前Cache已使用的内存大小,当超过最大容量时,从链表的头部开始移除元素
mutex_:保证每个Cache都是线程安全的
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_;
};
具体实现的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) ;
};
由于每一个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中进行相应的插入、移除等操作
}
由上可知,ShardedLRUCache最终都是调用LRUCache的相应操作来实现的,因此下面将详细介绍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) 调用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
}
这样就将一个节点插入到双向链表的尾部了
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;
}
当要插入一个元素时,首先进行查找:
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;
}
假设首先要插入元素的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值找到相应元素,最终来对找到的元素执行相关操作。
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会被移除,得到结果如下: