struct LRUHandle
是哈希表的一个节点类型及LRUCache中的链表节点类型
后面在LRUCache的LookUp中会使用
return reinterpret_cast<Cache::Handle*>(e)
强转为Handle这个空类对象,使用时再强转回LRUHandle,获取其void*类型的value(这个转换过程是为了啥,为什么不直接用LRUHandle?)
void* Value(Handle* handle) override {
return reinterpret_cast<LRUHandle*>(handle)->value;
}
结构定义:
struct LRUHandle {
void* value; // 这个value类型是TableAndFile
void (*deleter)(const Slice&, void* value); // 由用户传入
LRUHandle* next_hash; // 哈希表解决冲突的链表
LRUHandle* next; // lru/in_use双向链表指针
LRUHandle* prev; // lru/in_use双向链表指针
size_t charge; // TODO(opt): Only allow uint32_t?每条缓存纪录容量,累加为lrucache的usage_
size_t key_length; // key长度,与key_data一起构造key的slice
bool in_cache; // Whether entry is in the cache. Insert操作会触发写入in_cache=true,表明已经在哈希表中了
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,key_length是key的长度,配合使用
......
}
这里value类型定义为value*,需要用户在使用时进行一下转换,转到存入时的类型,例如:
Table* table = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
HandleTable
leveldb自己实现的哈希表,据称比g++内置hash表(随机读)快
解决冲突方式:开链法,尾插
成员
// 这个table是一些buckets的数组组成的,每个bucket是一个hash到bucket的缓存链表
uint32_t length_; // bucket数量,每个bucket中hash & (length_ - 1)
相同
uint32_t elems_; // key数量,等于length_时启动resize
LRUHandle** list_; // 指针数组,第一维为bucket,第二维是LRUHandle*类型指针,LRUHandle中的next_hash指向同一个slot中的下一个元素
插入:
// 哈希表插入,老节点替换为新节点返回查找到的key对应的老的LRUHandle节点或者插入新节点返回nullptr
// 如果不存在,那么从链表尾部插入后返回新节点,如果插入后表太大了就启动Resize
LRUHandle* Insert(LRUHandle* h) {
LRUHandle** ptr = FindPointer(h->key(), h->hash); // 返回hash和key都匹配的element,如果没有返回对应bucket的最后一个节点(nullptr)
LRUHandle* old = *ptr;
h->next_hash = (old == nullptr ? nullptr : old->next_hash); //
*ptr = h;
if (old == nullptr) {
++elems_;
if (elems_ > length_) { // 元素数量大于bucket数量,哈希表太大了,重新分配一下内存
// Since each cache entry is fairly large, we aim for a small
// average linked list length (<= 1).
// 扩容到新的大小为不小于elems_的2的幂次
Resize();
}
}
return old;
}
LRUCache
cache.cc
通过哈希表和链表实现的最近最少使用缓存
在这个类里维护了一个哈希表table_;两个环形双向链表,一个lru_,一个in_use_;双向链表指针为struct LRUHandle的成员prev和next
lru_为不常用的元素,容量不够用了会按写入顺序清空并删除其在哈希表中的对应元素
in_use_为常用的元素
class LRUCache {
......
private:
......
// Initialized before use.
// 初始化决定,哈希表的最大容量,如果usage_大于它了,就删元素
size_t capacity_;
// mutex_ protects the following state.
mutable port::Mutex mutex_;
size_t usage_ GUARDED_BY(mutex_);
// 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_); // LRU虚拟表头,保存最近没有在用的元素
// 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_); // in-use虚拟表头,保存最近使用的元素
HandleTable table_ GUARDED_BY(mutex_); // 哈希表
};
什么时候元素放入lru_:用户调用Release会执行UnRef,如果发现引用次数降到1了e->in_cache && e->refs == 1,就从in_use_删除放到lru_
什么时候元素放入in_use_: 执行lookup操作ref++,如果在lru_里面,那么从lru_删除并放入in_use_;insert操作都会节点链接到in_use_链表
什么时候彻底删除节点:新元素插入,老的相同key元素会被删除;用户调用Release执行UnRef,如果发现没有引用了即e->refs == 0,直接在链表和哈希表删除该节点
插入函数实现:
// 使用key构造一个新节点插入哈希表并放入in_use_链表头部
// 可能会触发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_);
// new一个新节点并填充字段
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. 返回的handle加一次ref
std::memcpy(e->key_data, key.data(), key.size());
// 节点放入in_use_,插入table_
if (capacity_ > 0) {
e->refs++; // for the cache's reference.cache的handle加一次ref
e->in_cache = true;
LRU_Append(&in_use_, e); // e->next = in_use_
usage_ += charge;
FinishErase(table_.Insert(e)); // 输入:老节点在哈希表中的entry指针或者nullptr,这个老节点已经被新节点替代,更新操作需要回收老元素
} 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;
}
// lru_非空,容量不够了,执行冷链lru_和hash表擦除操作直到容量不小于usage
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);
}
链表各种操作及引用计数:
// 如果在lru_,从lru_删除,添加到in_use_
// refs自增
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++;
}
// delete指针e(从in_use_删除放入lru_)
// 释放条件:refs自减后,1.refs为0,并且不在cache里面直接调用deleter删除
// 2.refs为1,并且在cache里,修改节点的pre/next指针将节点从原来的链表删除,将节点放入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); // 调用delete析构e
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);
}
}
// 从链表删除节点e
void LRUCache::LRU_Remove(LRUHandle* e) {
e->next->prev = e->prev;
e->prev->next = e->next;
}
// 把e节点添加到list链表尾部
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;
}
可以看到,in_use_ 和lru_链表在插入元素的时候都是从尾部append上去的,而lru_删除元素是从头部开始删除的,保证了最近使用的晚删除
LookUp:
// 在table_里查找key,会触发从lru_表向in_use_表迁移
// 返回值:找到返回指针,找不到返回nullptr
// 找到,refs自增1;在lru_的话,移动到in_use_
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);
}
Release:
// Lookup获取到Handle指针或Insert,使用完成后需要主动调用Release
// 将handle节点直接删除或者从in_use_删除放入lru_
void LRUCache::Release(Cache::Handle* handle) {
MutexLock l(&mutex_);
Unref(reinterpret_cast<LRUHandle*>(handle));
}
删除元素:
// e为旧纪录,这个函数处理元素删除操作,把e从链表删除,in_cache改为false,usage_减掉charge,删除节点e
bool LRUCache::FinishErase(LRUHandle* e) {
if (e != nullptr) {
assert(e->in_cache);
LRU_Remove(e); // 从next_/pre_这个链表删除e元素,这里不管它是在in_use_还是lru_
e->in_cache = false; //这里in_cache为false了,所以在Unref里面不会放入lru
usage_ -= e->charge;
Unref(e);
}
return e != nullptr;
}
ShardedLRUCache
由于LRUCache的哈希表随着冲突增大,以及LookUp和Insert等函数加锁操作随着查找次数上升性能下降,将LRUCache封装为了分片实现,即ShardedLRUCache
ShardedLRUCache继承自Cache,分shard的LRU,每个shard是一个LRUCache
默认16个shard,每个分片容量为(capacity + (16 -1)) / 16,使用哈希算法将key分散到不同shard
成员:
LRUCache shard_[kNumShards]; // 16个shard
port::Mutex id_mutex_; // last_id_的锁
uint64_t last_id_; // 没看到在哪用
ShardedLRUCache负责将每个key分配到不同的shard,这个过程是无锁的,分到不同shard之后LRUCache的操作有锁
TableCache
table_cache.h / table_cache.cc
leveldb使用的缓存,用于缓存ldb文件内容,先在LRU缓存查找(Lookup),找不到再读取lsb文件查找key,找到就insert到LRU中
void* value; // LRUHandle中这个value存取TableAndFile类型指针
成员变量:
Env* const env_;
const std::string dbname_;
const Options& options_;
Cache* cache_; // Cache是一个基类,在构造函数中使用NewLRUCache初始化它