【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; }