LRU原理
LRU 是一种经典的缓存淘汰策略,其原理以及实现可以查看我之前的博客LRU Cache 解析及实现。本文主要解析Leveldb LRU Cache。
Leveldb 实现
Leveldb 实现的LRUCache 使用自己实现的简单hashtable存储键值对,循环双链表记录每个元素的访问时间,为了提升多线程环境下的读写性能,Leveldb 内部使用LRUCache 数组对外提供服务。
Cache Entry
1 | struct LRUHandle { |
简单hash表
Leveldb LRU Cache 实现了线程安全的拉链式的hashtable1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88class HandleTable {
public:
HandleTable() : length_(0), elems_(0), list_(NULL) { Resize(); }
~HandleTable() { delete[] list_; }
// 查找,直接调用FindPointer方法
LRUHandle* Lookup(const Slice& key, uint32_t hash) {
return *FindPointer(key, hash);
}
// 插入,使用FindPointer方法找到插入位置,在hashtable 对位位置插入,当hashtable 中的元素个数大于桶的个数时候触发hashtable 的扩容
LRUHandle* Insert(LRUHandle* h) {
LRUHandle** ptr = FindPointer(h->key(), h->hash);
LRUHandle* old = *ptr;
h->next_hash = (old == NULL ? NULL : old->next_hash);
*ptr = h;
if (old == NULL) {
++elems_;
if (elems_ > length_) {
// Since each cache entry is fairly large, we aim for a small
// average linked list length (<= 1).
Resize();
}
}
return old;
}
// 删除hashtable中的元素,同样使用FindPointer方法定位到在hashtable 中的位置
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;
}
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_;
LRUHandle** list_;
// 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.
// 找到hash 元素的地址
LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
//找到hash 桶的位置,由于length_是2的指数次幂,所以使用 按位与替换mod操作进行加速
LRUHandle** ptr = &list_[hash & (length_ - 1)];
// 遍历hash桶中的单链表直到找到相应节点或单链表结束
while (*ptr != NULL &&
((*ptr)->hash != hash || key != (*ptr)->key())) {
ptr = &(*ptr)->next_hash;
}
return ptr;
}
// hash 表扩容
void Resize() {
// 每次扩容都是翻倍,保证hash桶的个数是2的指数次幂
uint32_t new_length = 4;
while (new_length < elems_) {
new_length *= 2;
}
// 创建一个新的hashtable
LRUHandle** new_list = new LRUHandle*[new_length];
memset(new_list, 0, sizeof(new_list[0]) * new_length);
uint32_t count = 0;
// 逐个桶迁移到新的hashtable
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;
LRUHandle** ptr = &new_list[hash & (new_length - 1)];
h->next_hash = *ptr;
*ptr = h;
h = next;
count++;
}
}
assert(elems_ == count);
// 释放掉老的hashtable
delete[] list_;
// 替换为新的hashtable
list_ = new_list;
length_ = new_length;
}
};
单个LRUCache实现
LRUCache 类声明
1 | class LRUCache { |
LRUCache 插入
1 | Cache::Handle* LRUCache::Insert( |
LRUCache 删除
1 | void LRUCache::Erase(const Slice& key, uint32_t hash) { |
LRUCache Prune
删除LRUCache中ref为1 的元素,最初插入的元素的引用为2,外部消费者对使用结束的元素ref减一。LRUCache中ref 为1 的handle表示此元素只在LRUCache中出现,外部并不使用。此方法的作用就是删除这样的元素。1
2
3
4
5
6
7
8
9
10
11
12void LRUCache::Prune() {
MutexLock l(&mutex_);
for (LRUHandle* e = lru_.next; e != &lru_; ) {
LRUHandle* next = e->next;
if (e->refs == 1) {
table_.Remove(e->key(), e->hash);
LRU_Remove(e);
Unref(e);
}
e = next;
}
}
LRU_Remove
删除双向链表中一个节点1
2
3
4void LRUCache::LRU_Remove(LRUHandle* e) {
e->next->prev = e->prev;
e->prev->next = e->next;
}
LRU_Append
插入到双向链表表头位置1
2
3
4
5
6
7void 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;
}
ShardedLRUCache
1 | static const int kNumShardBits = 4; |
总结
Leveldb 实现的LRUCahe 基于hashtable以及双向链表实现,为了提升性能以及保证跨平台特性,Leveldb 实现了一个线程安全的拉链式hashtable,此hashtable 的缺点是当触发扩容操作的时候需要将老hashtable 的全部元素复制到新的hashtable 中,这会期间会一直占用锁,此时多线程环境下的读写会阻塞。这一点Memcached 中实现的hashtable是有一个线程专门负责扩展,每次只扩展一个元素。
Leveldb 为了提升多线程环境下的读写性能,将固定容量的Cache分摊到多个LRUCache,每个LRUCache保证自己的线程安全,这就降低了多线程环境下锁的竞争。这种做法与分段锁的思想类似。