LevelDB源码阅读Cache
Posted Galaxy_hao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LevelDB源码阅读Cache相关的知识,希望对你有一定的参考价值。
是什么
leveldb内部实现的缓存
为什么要用
- 性能高于内嵌哈希表
学到什么
- 与(&)操作进行模运算
- 减少cache锁操作,可以分为多组cache
源码分析
LRUHandle
缓存中代表键值对的数据结构
// An entry is a variable length heap-allocated structure. Entries
// are kept in a circular doubly linked list ordered by access time.
struct LRUHandle {
void *value; // 存储键值对的值,可以为任意类型
void (*deleter)(const Slice &, void *value); // 函数指针当前结构体的清除函数
LRUHandle *next_hash; // 用户hash冲突使用
LRUHandle *next; // 当前节点下一个节点
LRUHandle *prev; // 当前节点上一个节点
size_t charge; // 当前节点占用内存
size_t key_length; // 当前节点键的长度
bool in_cache; // Whether entry is in the cache.
uint32_t refs; // References, including cache reference, if present.
uint32_t hash; // Hash of key(); used for fast sharding and comparisons
char key_data[1]; // Beginning of key
Slice key() const {
// next_ is only equal to this if the LRU handle is the list head of an
// empty list. List heads never have meaningful keys.
assert(next != this);
return Slice(key_data, key_length);
}
};
HandleTable
自定义的哈希表,首先看数据成员
// The table consists of an array of buckets where each bucket is
// a linked list of cache entries that hash into the bucket.
uint32_t length_; // hash表桶数组长度
uint32_t elems_; // 哈希表存储元素的数量
LRUHandle **list_; // 哈希数组指针,其中每个数组元素存储指针类型
构造函数和析构函数
HandleTable() : length_(0), elems_(0), list_(nullptr) { Resize(); }
~HandleTable() { delete[] list_; }
主要接口函数
- 查询
LRUHandle* Lookup(const Slice &key, uint32_t hash) {
return *FindPointer(key, hash);
}
其中FindPointer方法实现如下,首先确定在哈希数组的哪个哈希桶中,然后沿着哈希链表查找。
如果某个LRUHandle* 存在相同hash和key值,则返回LRUHandle*的二级指针,即上一个LRUHandle*的next_hash的二级指针。
如果不存在,则返回bucket中最后一个 LRUHandle*的next_hash,即为nullptr。
// Return a pointer to slot that points to a cache entry that
// matches key/hash. If there is no such cache entry, return a
// pointer to the trailing slot in the corresponding linked list.
LRUHandle** FindPointer(const Slice &key, uint32_t hash) {
LRUHandle **ptr = &list_[hash & (length_ - 1)]; // 下标结果是0到length-1
while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) {
ptr = &(*ptr)->next_hash; // 迭代哈希链找到键值相等节点,ptr为上一节点next_hash值
}
return ptr;
}
- 插入(插入示意图参考:LevelDB之Cache之HandleTable)
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; //将ptr值也就是对应上一个LRUHandle的next_hash值赋值为h来跳过old
if (old == nullptr) { // h是新节点
++elems_;
// Since each cache entry is fairly large, we aim for a small
// average linked list length (<= 1).
if (elems_ > length_) {
Resize();
}
}
return old;
}
其中Resize方法实现如下:
- 初始大小为4,每次以原有大小的2倍扩容
- 以新的长度创建哈希桶大小,并设置每个哈希桶为空
- 遍历原有每个哈希桶的每个元素,重新确定在新哈希桶的位置,采用头插法插入到新哈希桶的每个链表中
- delete原有哈希链表,并赋值为新的哈希链表
void Resize() {
uint32_t new_length = 4;
while (new_length < elems_) {
new_length *= 2; // 每次2倍扩容
}
LRUHandle **new_list = new LRUHandle *[new_length];
memset(new_list, 0, sizeof(new_list[0]) * new_length); //一级指针置空
uint32_t count = 0;
for (uint32_t i = 0; i < length_; ++i) {
LRUHandle *h = list_[i];
while (h != nullptr) {
LRUHandle *next = h->next_hash; // 一级指针上二级指针通过next_hash连接
uint32_t hash = h->hash;
LRUHandle **ptr = &new_list[hash & (new_length - 1)]; // 确定在新桶中位置
h->next_hash = *ptr; // 第一次*ptr为nullptr,其它为上个循环h的地址
*ptr = h;
h = next;
++count;
}
}
assert(elems_ == count);
delete[] list_;
list_ = new_list;
length_ = new_length;
}
- 删除
LRUHandle* Remove(const Slice &key, uint32_t hash) {
LRUHandle **ptr = FindPointer(key, hash);
LRUHandle *result = *ptr;
if (result != nullptr) { // 找到节点
*ptr = result->next_hash;
--elems_;
}
return result;
}
LRUCache
数据成员如下包括:
- cache总大小
- cache已经使用大小
- 冷链表(lru_),即在cache中,但没有被外部引用,cache容量满时会被淘汰
- 热链表(in_use_),即在cache中,同时又被外部引用,cache容量满时也不会被淘汰
- 哈希表(table_),用于快速查找(链表结构则用于快速插入和删除)
// Initialized before use.
size_t capacity_; // cache总容量
// mutex_ protects the following state.
mutable port::Mutex mutex_;
size_t usage_ GUARDED_BY(mutex_); // 已经使用的cache空间
// Dummy head of LRU list.
// lru.prev is newest entry, lru.next is oldest entry.
// Entries have refs==1 and in_cache==true.
LRUHandle lru_ GUARDED_BY(mutex_); // 双向循环链表
// Dummy head of in-use list.
// Entries are in use by clients, and have refs >= 2 and in_cache==true.
LRUHandle in_use_ GUARDED_BY(mutex_);
HandleTable table_ GUARDED_BY(mutex_); // 二级指针数组,用于缓存数据
构造函数和析构函数
构造函数,初始化所有数据成员,其中哈希执行默认初始化。
LRUCache::LRUCache() : capacity_(0), usage_(0) {
// Make empty circular linked lists.
lru_.next = &lru_;
lru_.prev = &lru_;
in_use_.next = &in_use_;
in_use_.prev = &in_use_;
}
执行析构函数时,热链表中不应该有元素,函数体主要删除冷链表中元素。
LRUCache::~LRUCache() {
assert(in_use_.next == &in_use_); // Error if caller has an unreleased handle
for (LRUHandle *e = lru_.next; e != &lru_;) {
LRUHandle *next = e->next;
assert(e->in_cache);
e->in_cache = false;
assert(e->refs == 1); // Invariant of lru_ list.
Unref(e);
e = next;
}
}
主要对外接口
- 查找
根据key和hash值在哈希表中进行查找,查找到后更新热链表。
Cache::Handle* LRUCache::Lookup(const Slice& key, uint32_t hash) {
MutexLock l(&mutex_);
LRUHandle* e = table_.Lookup(key, hash);
if (e != nullptr) {
Ref(e);
}
return reinterpret_cast<Cache::Handle*>(e);
}
- 插入
首先申请LRUHandle内存并且初始化,LRUHandle对象指针会返回给调用方,所以refs此时为1,然后添加到in_use_链表尾部,refs此时为2,同时更新in_cache=true和cache使用量。如果
插入到哈希表table_中已经存在,则会将原有节点从cache中删除。最后如果容量超限,从lru_进行淘汰并从哈希表中删除。
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;
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.
e->in_cache = true;
LRU_Append(&in_use_, e);
usage_ += charge;
FinishErase(table_.Insert(e)); // 插入到哈希表中同时清理旧节点
} else { // don‘t cache. (capacity_==0 is supported and turns off caching.)
// next is read by key() in an assert, so it must be initialized
e->next = nullptr;
}
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);
}
- 删除
从哈希表中删除,然后从cache链表删除。
void LRUCache::Erase(const Slice& key, uint32_t hash) {
MutexLock l(&mutex_);
FinishErase(table_.Remove(key, hash));
}
- 其它辅助方法
Ref方法表示要使用该cache,如果元素位于冷链表中,需要移到热链表。
void LRUCache::Ref(LRUHandle* e) {
if (e->refs == 1 && e->in_cache) { // If on lru_ list, move to in_use_ list.
LRU_Remove(e);
LRU_Append(&in_use_, e);
}
e->refs++;
}
Unref正好相反,表示用户不再使用该元素,需要将引用计数--,如果彻底没有用户使用,即引用计数为0,则删除这个元素,如果引用计数为1,则从热链表移到冷链表中。
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) {
// No longer in use; move to lru_ list.
LRU_Remove(e);
LRU_Append(&lru_, e);
}
}
其中从链表删除元素和将元素插入指定链表如下:
void LRUCache::LRU_Remove(LRUHandle* e) {
e->next->prev = e->prev;
e->prev->next = e->next;
}
void LRUCache::LRU_Append(LRUHandle* list, LRUHandle* e) {
// Make "e" newest entry by inserting just before *list
e->next = list;
e->prev = list->prev;
e->prev->next = e;
e->next->prev = e;
}
SharedLRUCache
为什么有SharedLRUCache?
LRUCache的接口都会加锁,为了更少的锁持有时间以及更高的缓存命中率,可以定义多个LRUCache,分别处理不同hash取模后的缓存处理。SharedLRUCache是LRUCache上的一层封装,管理16个cache,对外提供服务,外部调用接口都委托内部某个LRUCache。
数据成员
LRUCache shard_[kNumShards]; // 16个LRUCache
SharedLRUCache的主要作用就是计算Hash值,选择LRUCache,代码如下:
static inline uint32_t HashSlice(const Slice &s) {
return Hash(s.data(), s.size(), 0);
}
static uint32_t Shard(uint32_t hash) { // 得到shard_数组下标
return hash >> (32 - kNumShardBits); // 取hash高4位得到shard_数组下标
}
主要对外接口
这些接口都是先计算hash值,然后根据hash值选择LRUCache,最后通过调用LRUCache来实现。
- 插入
Handle* Insert(const Slice &key, void *value, size_t charge,
void (*deleter)(const Slice &key, void *value)) override {
const uint32_t hash = HashSlice(key);
return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
}
- 查找
Handle* Lookup(const Slice &key) override {
const uint32_t hash = HashSlice(key);
return shard_[Shard(hash)].Lookup(key, hash);
}
- 删除
void Erase(const Slice &key) override {
const uint32_t hash = HashSlice(key);
shard_[Shard(hash)].Erase(key, hash);
}
- 查看缓存使用大小
size_t TotalCharge() const override {
size_t total = 0;
for (int s = 0; s < kNumShards; s++) {
total += shard_[s].TotalCharge();
}
return total;
}
以上是关于LevelDB源码阅读Cache的主要内容,如果未能解决你的问题,请参考以下文章
LevelDB 源码剖析SSTable模块:SSTableBlock布隆过滤器LRU Cache