TableCache设计的出发点就是:提升性能。根据著名的局部性访问原理,leveldb设计了一个简单LRUCache算法,该算法是TableCache的核心,下面我们就来分析一下leveldb是如何实现的。
一、TableCache
先来看一下TableCache的类定义,非常简洁:
class TableCache {
public:
TableCache(const std::string& dbname, const Options* options, int entries);
~TableCache();
//迭代器
Iterator* NewIterator(const ReadOptions& options,
uint64_t file_number,
uint64_t file_size,
Table** tableptr = NULL);
// 查询接口
Status Get(const ReadOptions& options,
uint64_t file_number,
uint64_t file_size,
const Slice& k,
void* arg,
void (*handle_result)(void*, const Slice&, const Slice&));
// Evict any entry for the specified file number
// 根据文件编号 从cache中删除指定项
void Evict(uint64_t file_number);
private:
Env* const env_;
const std::string dbname_;
const Options* options_;
Cache* cache_; 封装LRUCache 在构造函数中初始化
//根据文件编号 查找文件
Status FindTable(uint64_t file_number, uint64_t file_size, Cache::Handle**);
};
1.1、查询接口
/**
* 从ldb中获取数据
* @param options 选项
* @param file_number ldb文件编号 来自FileMetaData
* @param file_size ldb文件大小 来自FileMetaData
* @param k 查找key值
* @param arg 回调函数参数
* @param saver 回调函数
*/
Status TableCache::Get(const ReadOptions& options,
uint64_t file_number,
uint64_t file_size,
const Slice& k,
void* arg,
void (*saver)(void*, const Slice&, const Slice&)) {
Cache::Handle* handle = NULL;
Status s = FindTable(file_number, file_size, &handle);// 打开ldb文件并且读取出data index block以及meta index block
if (s.ok()) {
Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
s = t->InternalGet(options, k, arg, saver);// 在table中查找k 如果找到则通过saver回调函数进行保存
cache_->Release(handle);//必须调用
}
return s;
}
这里强调一下:必须调用cache_->Release(handle)接口,查询接口十分的简单,代码注释也很清楚,下面来看一下FindTable具体实现 。
1.2、FindTable
/**
* 查找Table
* @param file_number ldb文件编号 来自FileMetaData
* @param file_size ldb文件大小 来自FileMetaData
* @param handle cache handle对象 输出参数
*/
Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
Cache::Handle** handle) {
Status s;
char buf[sizeof(file_number)];
EncodeFixed64(buf, file_number);
Slice key(buf, sizeof(buf));
*handle = cache_->Lookup(key);//按照key进行查找 如果没有找到则说明是新文件
if (*handle == NULL) {
std::string fname = TableFileName(dbname_, file_number);//读取ldb文件
RandomAccessFile* file = NULL;
Table* table = NULL;
s = env_->NewRandomAccessFile(fname, &file);
if (!s.ok()) {// 兼容 1.13版本(含)之前是sst文件作为后缀
std::string old_fname = SSTTableFileName(dbname_, file_number);
if (env_->NewRandomAccessFile(old_fname, &file).ok()) {
s = Status::OK();
}
}
if (s.ok()) {
s = Table::Open(*options_, file, file_size, &table);//打开文件 创建table对象
}
if (!s.ok()) {
assert(table == NULL);
delete file;
// We do not cache error results so that if the error is transient,
// or somebody repairs the file, we recover automatically.
} else {
TableAndFile* tf = new TableAndFile;//对文件对象和Table对象进行包装
tf->file = file;
tf->table = table;//保存table对象
//插入cache中 默认返回LRUHandle对象 此处key是文件编号
*handle = cache_->Insert(key, tf, 1, &DeleteEntry);
}
}
return s;
}
说明:
1)Cache 缓存的实际是ldb文件对象以及Table,可以通过Cache::Insert看出来。那么查询关键字是什么呢?是文件编号,也就是文件名字中的数字。
2)如果通过cache->Lookup没有找到说明是新文件,那么就要执行open操作,对ldb文件进行解析,这个解析过程是无法避免的而且是比较耗时的,这也就是为什么leveldb设计了一个LRUCache的原因(根据局部性原理)。
3)上面Table::Open实际是读取ldb文件并对其解析,例如解析出foot,index block,data block等,这部分是最好费性能的。
4)将file和table这两个对象封装到TableAndFile对象中并且将其插入到cache中。
二、ShardedLRUCache
类Cache只是接口,真正实现类为ShardedLRUCache,这部分内容比较凌乱,涉及到一些类,我对其进行了总结:
类名称 | 说明 | 备注 |
类Cache | 抽象类,几乎所有接口都是虚函数 | |
类ShardedLRUCache | 继承Cache,实现类 | |
类LRUCache | ShardedLRUCache定义了16分片,每个分片类型为LRUCache | 每个LRU是独立的,线程安全的,可以提升并发访问 |
结构体Handle | 空结构体,用于接口。内部实际为LRUHandle | |
结构体LRUHandle | 每个LRUHandle相当于cache中一个元素 | cache组织方式是双向链表,LRUHandle相当于链表中一个元素 |
类HandleTabble | 为了提升性能,leveldb还增加了HashTable用于存储LRUHandle |
由于SharedLRUCache方法基本上是对LRUCache方法的封装,所以这里只罗列两个方法,后面在介绍LRUCache的时候在详细介绍。
static const int kNumShardBits = 4;
static const int kNumShards = 1 << kNumShardBits;//16
explicit ShardedLRUCache(size_t capacity)
: last_id_(0) {
const size_t per_shard = (capacity + (kNumShards - 1)) / kNumShards;//每个分片最多缓存的个数
for (int s = 0; s < kNumShards; s++) {
shard_[s].SetCapacity(per_shard);//每个分片cache的容量为per_shard,超过这个就需要移除旧元素
}
}
virtual Handle* Insert(const Slice& key, void* value, size_t charge,
void (*deleter)(const Slice& key, void* value)) {
const uint32_t hash = HashSlice(key);//对key进行hash 然后插入到对应的cache中
return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
}
三、结构体LRUHandle
该结构体比较简单,注释已经给出详细说明。
struct LRUHandle {
void* value; //value
void (*deleter)(const Slice&, void* value);
LRUHandle* next_hash;// 在hashtable中使用
LRUHandle* next; //双向链表
LRUHandle* prev; //双向链表
size_t charge; // 占用cache空间数目,目前始终为1
size_t key_length;
bool in_cache; // 表示当前元素是否在cache中 false表示回收内存.
uint32_t refs; // 当引用计数为0时就要删除
uint32_t hash; // Hash of key(); used for fast sharding and comparisons
char key_data[1]; // key值
Slice key() const {
// For cheaper lookups, we allow a temporary Handle object
// to store a pointer to a key in "value".
if (next == this) {
return *(reinterpret_cast<Slice*>(value));
} else {
return Slice(key_data, key_length);
}
}
};
四、LRUCache
下面来看一下LRUCache定义以及相关内容介绍。
// A single shard of sharded cache.
class LRUCache {
public:
LRUCache();
~LRUCache();
// Separate from constructor so caller can easily make an array of LRUCache
// 默认值是62
void SetCapacity(size_t capacity) { capacity_ = capacity; }
// Like Cache methods, but with an extra "hash" parameter.
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);
void Prune();
size_t TotalCharge() const {
MutexLock l(&mutex_);
return usage_;
}
private:
void LRU_Remove(LRUHandle* e);
void LRU_Append(LRUHandle*list, LRUHandle* e);
void Ref(LRUHandle* e);
void Unref(LRUHandle* e);
bool FinishErase(LRUHandle* e);
// Initialized before use. 默认值是62
size_t capacity_; //容量 超过这个容量后就要移除旧数据
// mutex_ protects the following state.
mutable port::Mutex mutex_;
size_t usage_; //当前cache使用量
// Dummy head of LRU list.
// lru.prev is newest entry, lru.next is oldest entry.
// Entries have refs==1 and in_cache==true.
// 稳定状态下 所有元素都存在这个链表中 这个链表中元素refs一定等于1
LRUHandle lru_;
// Dummy head of in-use list.
// Entries are in use by clients, and have refs >= 2 and in_cache==true.
// 当用户查询某条记录时 会将元素从lru_移动到这个链表中 这个链表中元素refs一定大于等于2 当使用完毕后
// refs自减 然后将元素移回到lru_中
LRUHandle in_use_;
HandleTable table_; /* 元素始终存在hashtable 只有从LRUCache删除时才会吧hashtable中元素删除 */
};
说明:
1)每个LRUCache都有一定容量,当存储的容量(usage_)超过阈值(capacity_)后就需要删除旧数据。这一点就是LRU思想。
2)LRUCache有两个链表和一个HashTable(请看注释内容),分别为:in_use_,lru_,table_
名称 | 作用 |
in_use_ | 所有的新元素都会放到这个链表中 |
lru_ | 从in_use_中移除的元素会暂时保存在lru_中 |
table_ | hashTable,所有元素都会存在LRUHandle,用于查询接口Lookup。 |
特别说明:
1)一个元素会存储到hashtable中并且会存储到某个链表中(两个链表中的一个)。
2)新元素一定先存储到in_use_链表中,满足某些条件后会将元素移植到lru链表中,具体看下面分析。
3)当链表元素从lru_中删除,同时需要从hashtable中删除掉(彻底删除了)。
4.1、Insert插入
/**
* 插入LRUCache中
* @param key 关键字
* @param hash hash值
* @param value value值
* @param charge
* @param deleter 回调函数
*/
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中
LRUHandle* e = reinterpret_cast<LRUHandle*>(
malloc(sizeof(LRUHandle)-1 + key.size()));
e->value = value;
e->deleter = deleter;
e->charge = charge;//占用cache容量 目前始终为1
e->key_length = key.size();
e->hash = hash;
e->in_cache = false;
e->refs = 1; // for the returned handle.
memcpy(e->key_data, key.data(), key.size());
if (capacity_ > 0) {
e->refs++; // for the cache's reference. 注意这里refs已经变成2
e->in_cache = true;
LRU_Append(&in_use_, e);//插入链表
usage_ += charge; //统计使用量
FinishErase(table_.Insert(e));//插入hashtable中
} // else don't cache. (Tests use capacity_==0 to turn off caching.)
/* 删除LRU链表中所有节点 */
while (usage_ > capacity_ && lru_.next != &lru_) {
LRUHandle* old = lru_.next;
assert(old->refs == 1);
bool erased = FinishErase(table_.Remove(old->key(), old->hash));//先从hashtable中删除
if (!erased) { // to avoid unused variable when compiled NDEBUG
assert(erased);
}
}
return reinterpret_cast<Cache::Handle*>(e);
}
说明:
1)默认capacity一定是大于0的,所以新元素会插入到in_use_链表中。
2)table_.Insert是将新元素插入到hashTable中,但是当hashTable中有重复元素(hash值相同&&key值相同)就会把旧元素返回回来。旧元素作为FinishErase输入参数,执行后续流程。
3)LRUCache使用场景有两个地方:open ldb文件和存储data block时。这两中场景不太相同,在open ldb文件这种场景下table_.insert返回值一定是null,因为key是文件编号,leveldb不可能打开同一个文件两次。 但是对于读取data block的时候却有可能出现table_.insert返回非空,比如user-key更新操作,返回飞控。
4)while表示in_use_链表存储已经达到最大,这个时候需要对LRU链表进行回收释放。
4.2、FinishErase
// If e != NULL, finish removing *e from the cache; it has already been removed
// from the hash table. Return whether e != NULL. Requires mutex_ held.
bool LRUCache::FinishErase(LRUHandle* e) {
if (e != NULL) {
assert(e->in_cache);
LRU_Remove(e);//从in_use_链表中删除
e->in_cache = false;
usage_ -= e->charge; //减小使用计数
Unref(e);
}
return e != NULL;
}
4.3、引用计数器
/**
* 将引用计数加1
* @param e
*/
void LRUCache::Ref(LRUHandle* e) {
if (e->refs == 1 && e->in_cache) {//进入这个分支表示e一定在lru_链表中If on lru_ list, move to in_use_ list
LRU_Remove(e);
LRU_Append(&in_use_, e);
}
e->refs++;
}
/**
* 释放引用计数
* @param e 元素
* @说明:
* 要么直接回收内存 要么将元素e从in_use_链表移动到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);
free(e);
} else if (e->in_cache && e->refs == 1) {
//可参考FindTabe函数 会进入这个分支 No longer in use; move to lru_ list.
LRU_Remove(e);
LRU_Append(&lru_, e);//插入LRU链表中
}
}
通常情况下,元素e一定在lru_链表中,只有需要使用元素e的时候才把元素e从lru_中移动到in_use_链表中,这里的Unref方法需要注意,这里是唯一的地方将元素e插入到lru链表中。
五、总结
至此,leveldb的LRUCache相关内容介绍完毕,还剩下一部分方法没有介绍,大家可自行阅读。