文件
cache.h
cache.cc
LRUCache的最基本的单位是struct LRUHandle,用来存放数据和一系列指针。
LRUCache里有一个LRUHandle组成的LRU链表和一个LRUHandle组成的HandleTable对象存放数据。
先看handle
-
struct LRUHandle {
-
void* value; // 存放数据
-
void (*deleter)(const Slice&, void* value); // 用于删除数据的回调函数
-
LRUHandle* next_hash; // 哈希捅内的下一个handle
-
LRUHandle* next; // 当handle作为lru链表的节点时使用
-
LRUHandle* prev; // 当handle作为lru链表的节点时使用
-
size_t charge; // TODO(opt): Only allow uint32_t? // 调用者指定的,清理cache时用到。
-
size_t key_length;
-
uint32_t refs; // 引用计数
-
uint32_t hash; // 哈希值 // Hash of key(); used for fast sharding and comparisons
-
char key_data[1]; // 这个和key_length一起组成leveldb自己实现的字符串Slice类 // Beginning of key
-
-
// 将key_data, key_length组成leveldb自己实现的字符串Slice类,作为key返回
-
// 当链表里只有自己,也就是只把LRUHandle当一个简单的数据容器时,就返回value。
-
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);
-
}
-
}
-
};
cache不直接保存LRUHandle,而是将LRUHandle组成HandleTable来保存
HandleTable主要功能就是维护LRUHandle组成的哈希表。
私有成员变量:
-
private:
-
// 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_; // 哈希捅的个数
-
uint32_t elems_; // 总的handle个数
-
LRUHandle** list_; // 指向若干handle的指针数组,每handle是一个链表,俗称哈希桶,hash bucket。
下面看这个哈希表怎么实现的
-
/*
-
三个私有成员变量置空,函数内容只有一句 Resize(),这个是给哈希表扩容用的。
-
根据内部逻辑,现在elems_是0,就直接将list_初始化一个长度为4的指针数组。
-
*/
-
HandleTable() : length_(0), elems_(0), list_(NULL) { Resize(); }
看查找操作最能体现哈希表的结构
-
LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
-
/*
-
首先创建一个临时的指针,根据要查找哈希值定位哈希捅,也就是指针数组list_的一个元素。
-
通过哈希值和list_元素数(length_ - 1)进行"与"运算,可以随机得到一个小于等于(length_ - 1)的整数,这样可以高效定位哈希桶。
-
所以说在没有哈希碰撞的情况下,哈希表的查找效率非常高。
-
由于list_是指针数组,每个元素都是一个指针。list_[hash & (length_ - 1)]本身就是一个指针。
-
为了能在函数外面直接通过ptr维护list_,需要将ptr定义为一个二级指针指向list_中某个指针的地址。
-
*/
-
LRUHandle** ptr = &list_[hash & (length_ - 1)];
-
-
/*
-
每一个哈希桶是一个链表
-
定位到第哈希表的某一个链表之后,在这一个链表里查找,找到哈希值和具key值都一样的,指针就不再移动了。
-
这里分两组情况
-
1 链表的第一个handle就是要找的,指针不需要移动,这时ptr是执行这个handle指针的指针。
-
2 ptr需要在链表里移动,那么最终ptr会定位到链表里某个handle的next_hash这个指针。
-
这里又有两种情况,就是如果next_hash指向的handle就是要找的handle,另一种情况是没有找到,这时next_hash执行的是NULL。
-
但不管怎样,将来对(*ptr)的赋值操作,就是改变handle或者handle->next_hash这两个指针的指向。
-
*/
-
while (*ptr != NULL &&
-
((*ptr)->hash != hash || key != (*ptr)->key())) {
-
ptr = &(*ptr)->next_hash; // next_hash执行的就是哈希值相同的下一个handle,也就是这一行的下一个。
-
}
-
return ptr; // 返回二级指针ptr,让函数外面可以通过操作ptr来操作哈希表的数据。
-
}
插入操作
-
LRUHandle* Insert(LRUHandle* h) {
-
/*
-
先查找,可以定位到相同数据的已有handle。
-
这里返回的是二级指针,指向了指向list_里某个哈希桶的第一个handle指针的指针,或者是指向这个哈希桶里某个handle的next_hash的指针。
-
如果哈希表里没有相同handle,ptr会指向这个哈希桶最后一个handle的next_hash这个指针本身。
-
因此操作*ptr就是操作哈希桶本身。
-
*/
-
LRUHandle** ptr = FindPointer(h->key(), h->hash);
-
-
/*
-
ptr是二级指针,因此*ptr是找到的handle的前一个handle的next_hash这个指针。
-
因此old被赋值后,就是找到的handle
-
*/
-
LRUHandle* old = *ptr;
-
/*
-
leveldb里的容器有一个特点,就是不允许重复值
-
old->next_hash指的是找到的handle的next_hash
-
让h->next_hash指向old的next_hash,再让*ptr指向h,其实是把找到的handle从链表里摘掉了,用h替换old。
-
*ptr是已有handle的next_hash。*ptr = h的操作实际上是让已有handle的上一个handle的next_hash指向h.
-
*/
-
h->next_hash = (old == NULL ? NULL : old->next_hash);
-
*ptr = h;
-
-
// 如果没找到重复值,就新建一个handle,并且按需要对哈希表扩容
-
if (old == NULL) {
-
++elems_;
-
/*
-
每次插入后,判断handle数是否大于行数。
-
如果handle数大于哈希桶数,则调用resize函数对哈希表进行扩容,扩充哈希桶数,保证每个哈希桶最多一条记录。
-
*/
-
if (elems_ > length_) {
-
// Since each cache entry is fairly large, we aim for a small
-
// average linked list length (<= 1).
-
Resize();
-
}
-
}
-
// 将old返回很重要,因为这个被摘到的handle需要在函数外面销毁。
-
return old;
-
}
删除和插入操作原理类似,很简单
-
LRUHandle* Remove(const Slice& key, uint32_t hash) {
-
LRUHandle** ptr = FindPointer(key, hash);
-
LRUHandle* result = *ptr;
-
if (result != NULL) {
-
*ptr = result->next_hash;
-
--elems_;
-
}
-
return result;
-
}
对哈希表的扩容,就是新建一个哈希桶数更多的list_,将所有handle重新排布到更多的哈希桶里。
-
void Resize() {
-
// 初始化时是4个哈希桶
-
uint32_t new_length = 4;
-
//重新决定哈希桶的数量
-
while (new_length < elems_) {
-
new_length *= 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 != NULL) {
-
LRUHandle* next = h->next_hash;
-
uint32_t hash = h->hash;
-
// 很据先有handle的哈希值定位新的哈希桶
-
LRUHandle** ptr = &new_list[hash & (new_length - 1)];
-
/*
-
下面是二级指针操作
-
1 哈希桶是空的的时候,*ptr指向NULL,让h->next_hash指向NULL
-
2 当哈希桶里有东西的时候,*ptr指向哈希桶里第一个handle,在这个函数里就是上一次循环的h
-
让h->next_hash指向哈希桶第一个handle
-
让指向哈希桶第一个handle的指针指向h
-
*/
-
h->next_hash = *ptr;
-
*ptr = h;
-
-
// h在老哈希桶里向后移动
-
h = next;
-
count++; // 新哈希表里的handle数加1
-
}
-
}
-
assert(elems_ == count);
-
-
删除老的指针数组,将list_指向新的。
-
delete[] list_;
-
list_ = new_list;
-
length_ = new_length;
-
}
-
};
LRUCache
最重要的两个私有变量
-
// 由handle组成的lru链表,最近访问的数据在最前端。
-
LRUHandle lru_;
-
// 用哈希表存放数据
-
HandleTable table_;
查找操作非常简单,就是调table_.Lookup,而table_.Lookup只是对FindPointer的简单调用
-
Cache::Handle* LRUCache::Lookup(const Slice& key, uint32_t hash) {
-
MutexLock l(&mutex_);
-
LRUHandle* e = table_.Lookup(key, hash);
-
-
// cache的查找比哈希表多两点,一个是更新handle的引用计数,另一个是将handle从lru摘除,加到最近访问的顶端。
-
if (e != NULL) {
-
e->refs++;
-
LRU_Remove(e);
-
LRU_Append(e);
-
}
-
return reinterpret_cast<Cache::Handle*>(e);
-
}
LRU_Remove和LRU_Append都是简单的指针操作
下面是LRU_Append。leveldb的lru是循环链表,规则是:表头的next是最冷端,表头的priv是最近访问的。
-
void LRUCache::LRU_Append(LRUHandle* e) {
-
// Make "e" newest entry by inserting just before lru_
-
e->next = &lru_;
-
e->prev = lru_.prev;
-
e->prev->next = e;
-
e->next->prev = e;
-
}
LRUCache里值得一看的是insert操作。
-
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_); // 保证线程安全,先获取mutex
-
-
// 将参数封装成handle对象。
-
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->refs = 2; // One from LRUCache, one for the returned handle
-
memcpy(e->key_data, key.data(), key.size());
-
-
// 新handle加到lru顶端
-
LRU_Append(e);
-
usage_ += charge;
-
-
LRUHandle* old = table_.Insert(e);
-
// table_.insert 用e替换老的old,old需要手动销毁。
-
if (old != NULL) {
-
LRU_Remove(old);
-
Unref(old);
-
}
-
-
/*
-
下面这段是LRUCache的关键
-
lru链表的结构比较特殊,lru_.next永远指向最冷,最长时间没人访问的handle
-
每次插入新handle前,调用者会手工指定charge。
-
插入时usage_会+=handle的charge,
-
当usage_超过调用者定义的容量(capacity_)时就要从冷端开始清理数据
-
这个机制可以让调用者通过控制capacity_和新handle的charge来调整lru链表的清理行为
-
*/
-
while (usage_ > capacity_ && lru_.next != &lru_) {
-
LRUHandle* old = lru_.next;
-
LRU_Remove(old);
-
table_.Remove(old->key(), old->hash);
-
Unref(old);
-
}
-
-
return reinterpret_cast<Cache::Handle*>(e);
-
}
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/26239116/viewspace-1842049/
,如需转载,请注明出处,否则将追究法律责任。