LevelDB 学习笔记-Cache
Cache
Cache 是一个抽象类,里面定义了一系列操纵 Cache 的方法,而 ShardedLRUCache 是 Cache 的实现类,可以使用 NewLRUCache(size_t capacity) 创建一个新 ShardedLRUCache 实例。
Handle是在Cache中定义的一个空结构体,但是对用户来说,它代表了一个 Cache 缓存项的凭依。啥意思呢?就是说用户可以调用Insert或Lookup方法获取一个 Handle,也可以调用Release方法释放该缓存项,但是用户是不能够直接操作Handle的,因为它没有定义任何属性和方法。
需要注意的是,在 Insert 方法中需要传入缓存项的大小 charge,当超出缓存容量时会将部分缓存淘汰。Insert 中还需要提供一个 deleter 函数,该函数以 key 和 value 作为参数,当缓存项的引用计数变为 0 时会调用该方法
LRUCache
LRUCache 是一个 LRU Cache 的实现,里面包含了两个双向链表 lru_ 和 in_use_。其中 lru_ 链表按照最近使用的顺序存放着存在于缓存中,但是没有使用的缓存项;in_use_ 存放着正在使用的缓存项。
LRUCache 中的缓存项可以通过 LRUHandle 进行管理,其内部有一个 ref_ 引用计数,当缓存项被使用时,需要调用 Ref 将引用计数加一,这时会将缓存项移除并加入到 in_use_ 链表中。当引用计数变为 1 时,会从 in_use_ 中移除并加入到 lur_ 链表中。当引用计数变为 0 时,会释放该缓存项。可以理解为当缓存项正在使用时,就将其 pin 注,这样就不能将其淘汰了
HandleTable是一个 Hash Table,其内部实现采用的是链地址法。HandleTable的使用目的是加速访问缓存项,其内部包含lru_和in_use_中的所有元素。刚创建时其数组容量为 0,当第一次插入时,会扩容为 4。当容量超过 4 时会扩容为 8,当容量减少到小于 4 时,会缩容为 4
ShardedLRUCache
ShardedLRUCache 是 Cache 的实现类,从名字也可以看出来,ShardedLRUCache 是一个分片的缓存,每一部分是一个 LRUCache。ShardedLRUCache 通过 Shard(uint32_t hash) 来决定某个 key 应该放入哪个分片,其实现只是简单的讲 key 的 hash 值右移 28 位(即取 uint32_t 值的高 4 位)。ShardedLRUCache 的内部实现也很简单,基本上只是调用了 LRUCache 的方法而已
为什么要进行分片?
每个 LRUCache 都有自己独立的锁,将 cache 进行分片能够减少锁的争用,增加并行性
{% endnote %}
Cache以及其相关实现都是线程安全的,因此上层用户可以放心地使用
TableCache
TableCache 是对 Cache 的包装,并作为 VersionSet 的成员变量对 Table 进行缓存。
class TableCache {
public:
TableCache(const std::string& dbname, const Options& options, int entries);
TableCache(const TableCache&) = delete;
TableCache& operator=(const TableCache&) = delete;
~TableCache();
// Return an iterator for the specified file number (the corresponding
// file length must be exactly "file_size" bytes). If "tableptr" is
// non-null, also sets "*tableptr" to point to the Table object
// underlying the returned iterator, or to nullptr if no Table object
// underlies the returned iterator. The returned "*tableptr" object is owned
// by the cache and should not be deleted, and is valid for as long as the
// returned iterator is live.
Iterator* NewIterator(const ReadOptions& options, uint64_t file_number,
uint64_t file_size, Table** tableptr = nullptr);
// If a seek to internal key "k" in specified file finds an entry,
// call (*handle_result)(arg, found_key, found_value).
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
void Evict(uint64_t file_number);
private:
Status FindTable(uint64_t file_number, uint64_t file_size, Cache::Handle**);
Env* const env_;
const std::string dbname_;
const Options& options_;
Cache* cache_;
};
当 VersionSet 需要使用 Table 时,会通过 Get 方法先从 TableCache 中获取,查找的具体逻辑实在 FindTable() 中实现的。如果 cache 中没有该 table,FindTable 打开该 table,并将其插入到 cache 中的 in_use_ 链表中。当使用完毕后,会调用 Cache::Release 方法将其移动到 lru_ 链表中
Status TableCache::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&)) {
Cache::Handle* handle = nullptr;
Status s = FindTable(file_number, file_size, &handle);
if (s.ok()) {
Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
s = t->InternalGet(options, k, arg, handle_result);
cache_->Release(handle); // 从 in_use_ 链表移动到 lru_ 链表
}
return s;
}
void (*handle_result)(void*, const Slice&, const Slice&)的作用是传递给Table#InternalGet,在这个方法中会调用传递的handle_result方法
这里我们只关注于缓存相关的实现,所以我们现在只关注 FindTable 方法
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); // 从缓存中查找
if (*handle == nullptr) { // 如果没找到,就打开文件,并插入 cache
std::string fname = TableFileName(dbname_, file_number);
RandomAccessFile* file = nullptr;
Table* table = nullptr;
s = env_->NewRandomAccessFile(fname, &file);
s = Table::Open(options_, file, file_size, &table);
// 插入 cache
TableAndFile* tf = new TableAndFile;
tf->file = file;
tf->table = table;
*handle = cache_->Insert(key, tf, 1, &DeleteEntry);
}
return s;
}
Table
Table 是 SSTable 的内存形式,在之前已经介绍过 SSTable 的格式了,这里只着重看 Table 的内存形式。
Table 只包含了一个成员变量 Rep,其中包含了 index_block 和 metaindex_handle 以及 filter 和 filter_data,在打开一个 Table 时都会为这些变量赋值。只有两个地方调用了 Open 方法,一个是 FindTable,另一个是 DumpTable,下面我们来看一下 Open 方法
Status Table::Open(const Options& options, RandomAccessFile* file,
uint64_t size, Table** table) {
*table = nullptr;
char footer_space[Footer::kEncodedLength];
Slice footer_input;
Status s = file->Read(size - Footer::kEncodedLength, Footer::kEncodedLength,
&footer_input, footer_space); // 读取 Footer
Footer footer;
s = footer.DecodeFrom(&footer_input);
// Read the index block
BlockContents index_block_contents;
ReadOptions opt;
if (options.paranoid_checks) {
opt.verify_checksums = true;
}
// 读取 index block
s = ReadBlock(file, opt, footer.index_handle(), &index_block_contents);
if (s.ok()) {
// We've successfully read the footer and the index block: we're
// ready to serve requests.
Block* index_block = new Block(index_block_contents);
Rep* rep = new Table::Rep;
rep->options = options;
rep->file = file;
rep->metaindex_handle = footer.metaindex_handle();
rep->index_block = index_block;
rep->cache_id = (options.block_cache ? options.block_cache->NewId() : 0);
rep->filter_data = nullptr;
rep->filter = nullptr;
*table = new Table(rep);
(*table)->ReadMeta(footer);
}
return s;
}
Footer 中包含了 metaindex_handle 以及 index_handle,可以参考 SSTable 那篇文章
在 FindTable 中打开 Table 后会放入缓存中,也就是说 TableCache 的缓存项中包含了 Table 这个结构,其中比较重要的是 index_block 这个结构。index_block 中存放的是每个 Data Block 中的 last key,通过查找 index block 可以知道某个 key 是否在该 table 的 key 的范围中,以及该 key 在该文件的哪个 data block 中。如果制定了 filter 还会通过 filter 进一步判断 table 是否有某个 key。
从 Open() 方法中我们可以看到 Options 结构还有一个 block_cache 字段。这个字段是一个 Cache 结构,默认为 ShardedLRUCache,默认大小为 8 M,在 DBImpl 的构造函数中创建,用户也可以自定义 Cache。
cache_block 主要作用是将当前迭代的 block 放入缓存中,具体存放时机在 Table::BlockReader 方法中。这一点在迭代的时候很有用,否则就需要从文件中读取了。
综上,缓存的作用主要体现在以下几点:
- 缓存 Table,通过 Table 中的 index block 和 filter 判断 Key 是否在 table 中,从而减少 I/O 的次数
- 缓存 Block,迭代 block 时将 block 缓存,从而减少 I/O