0%

levelDB源码剖析(13)--缓存Cache

缓存Cache

源码位置

include/leveldb/cache.h: 定义Cache接口
util/cache.cc: 实现LRU缓存
table/table.cc: 读取Data Block时使用缓存
db/table_cache.cc:实现一个Table结构的缓存

正如存储器山所描述的那样,各个存储器之间的速度差距非常显著。所以对访问速度较慢的部分使用缓存是非常有必要的。

其中在levelDB中,主要有TableCache和BlockCache这两种缓存。TableCache主要是缓存SST文件里面的data block index,而BlockCache主要是缓存data block。

LevelDB提供了一个Cache接口,用户可以实现自己的缓存方式。默认提供了一个LRU Cache,缓存最近使用的数据。

缓存的接口

我们首先来看一下Cache的接口类,同样,这也是一个纯虚基类,官方默认提供了一个使用LRU策略的Cache。

1
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

// 创建一个有固定容量的基于LRU策略的缓存并返回
LEVELDB_EXPORT Cache *NewLRUCache(size_t capacity);

class LEVELDB_EXPORT Cache
{
public:
// 构造析构函数...
Cache() = default;
Cache(const Cache &) = delete;
Cache &operator=(const Cache &) = delete;
// Destroys all existing entries by calling the "deleter"
// function that was passed to the constructor.
// 析构函数需要调用deleter函数来销毁所有的entries
virtual ~Cache();

// cache中的entry的句柄。
struct Handle
{
};

// Insert a mapping from key->value into the cache and assign it
// the specified charge against the total cache capacity.
//
// Returns a handle that corresponds to the mapping. The caller
// must call this->Release(handle) when the returned mapping is no
// longer needed.
//
// When the inserted entry is no longer needed, the key and
// value will be passed to "deleter".
// Insert将一个键值对映射插入到cache中,并赋予相应的“费用”charge.
// Insert返回该映射对应的handle.调用者不再使用时必须调用Release(handle)方法。
// 当不再需要此entry时,键值对会被传到deleter.
virtual Handle *Insert(const Slice &key, void *value, size_t charge,
void (*deleter)(const Slice &key, void *value)) = 0;

// If the cache has no mapping for "key", returns nullptr.
//
// Else return a handle that corresponds to the mapping. The caller
// must call this->Release(handle) when the returned mapping is no
// longer needed.
// Lookup返回对应key的handle,如果没有则返回nullptr.
// 调用者不再使用时必须调用Release(handle)方法。
virtual Handle *Lookup(const Slice &key) = 0;

// Release a mapping returned by a previous Lookup().
// REQUIRES: handle must not have been released yet.
// REQUIRES: handle must have been returned by a method on *this.
// Release释放一个mapping.
virtual void Release(Handle *handle) = 0;

// Value封装Lookup()返回的handle中的值并返回之。
virtual void *Value(Handle *handle) = 0;

// Erase将一个entry删去。只有在所有与之关联的handle释放后才会被真正删去。
// 即引用计数
virtual void Erase(const Slice &key) = 0;

// Return a new numeric id. May be used by multiple clients who are
// sharing the same cache to partition the key space. Typically the
// client will allocate a new id at startup and prepend the id to
// its cache keys.
// 返回一个ID,可能被多个共用相同缓存的客户端同时使用
// 他们共享同一个cache来划分key space.
// 通常client会在启动时分配一个新的id,并将其id放到缓存的键里。
virtual uint64_t NewId() = 0;

// 移除掉所有没有被使用的缓存
virtual void Prune() {}

// 返回当前缓存的大小
virtual size_t TotalCharge() const = 0;
};

这也是一个纯虚基类,定义了接口,下面来具体阐述其接口的含义。

首先,Cache本身作为一个缓存容器,可以被多个客户端调用(或者说多个线程),考虑到有些线程会把相同的数据放入缓存中,这些相同的数据本身也不需要缓存多分,所以采用引用计数的方式就能对其进行很好的引用和销毁。

其次,作为一个容器,插入(Insert),查找(Lookup),销毁(Erase)这些对缓存进行操作的接口是必不可少的。

其中,由于销毁一个缓存涉及到多线程中共享引用的问题,所以只有一个缓存中的引用计数为1(即只被缓存本身引用时),才会真正销毁。

另外,一般而言,多线程访问同一个数据空间时需要加锁,但是这样会降低效率,所以需要一种更巧妙的方法来实现不加锁的实现。为了区别多个线程所以需要为这些线程分配ID来进行标识(NewId),至于具体怎么做后面在ShardedLRUCache中会具体阐述。

但是,考虑到一些操作必须进行加锁处理,所以有了加锁后释放的方法(Release)。

以及,缓存大小是有限的,不能无限充,所以扩需要及时检测缓存大小(TotalCharge),并适时清理不必要的缓存项(Prune)。

默认的LRUCache

levelDB中默认提供的Cache就是LRUCache。不过由于其实现比较多,我们先不看其实现,而是大致描述一下其组成。

LRUCache的实现有以下特点:

  • 每一个缓存项都保存在一个LRUHandler里
  • 每一个LRUHandler首先被保存在一个哈希表table_里面,支持根据键快速的查找
  • LRUCache里面有两个双向循环链表lru_和in_use_,每一个LRUHandler可以在两个链表中的一个里,但是不会同时在两个里,也有可能有些LRUHandler被淘汰出缓存了,不在任何链表上
  • in_use_保存当前正在被引用的LRUHandler,这个链表主要是为了检查
  • lru_保存没有被使用的LRUHandler,按照访问顺序来保存,lru_.next保存最旧的,lru_.prev保存最新的,需要淘汰缓存时,会从lru_里的next开始淘汰
  • 当一个LRUHandler被使用时,会从lru_移动到in_use_,使用完成后,会从in_use_重新移动到lru_里
  • 每个LRUCache都有一个容量capacity_,表示这个缓存的大小,每次插入一个项时都会指定这个缓存项的大小,更新usage_字段,当usage_超过capacity_时,就淘汰最旧的缓存项,直到低于capacity_

另外,其使用引用计数来进行管理,具体流程如下:

  • 当一个LRUHandler被加入到缓存里面,并且没有被使用时,计数为1
  • 如果客户端需要访问一个缓存,就会找到这个LRUHandler,调用Ref,将计数加1,并且当此时缓存在lru_里,就移动到in_use里
  • 当客户端使用完一个缓存时,调用Unref里,将计数减1,当计数为0时,调用回调函数销毁缓存,当计数为1时,移动到in_use里面
  • 这样可以自动控制缓存的销毁,当一个LRUHandler被移出缓存时,如果还有其他的引用,也不会被销毁

查找一个缓存的流程如下:

  • 通过哈希表查找对应的LRUHandler
  • 如果找到了,调用Ref,返回缓存项
  • 使用完缓存项后,调用Release释放缓存

插入缓存需要将缓存项插入到哈希表以及链表中,并且更新容量,如果缓存容量过多,需要淘汰旧缓存。插入一个缓存项的步骤如下:

  • 生成一个LRUHandler保存缓存的内容,计数为1
  • 再将计数加1,表示当前缓存项被当前客户端引用,插入到in_use_链表中
  • 插入时会指定插入项的大小更新usage_字段
  • 插入到哈希表中
  • 如果有相同值旧的缓存项,释放旧项
  • 判断容量是否超标,如果超标,释放最旧的缓存项,直到容量不超标为止

LRUHandle

我们先来看一下LRUHandle,前面也说了,每一个缓存项都保存在LRUHandler中。

1
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

namespace // 位于一个匿名名称空间中
{
// LRU cache implementation
//
// Cache entries have an "in_cache" boolean indicating whether the cache has a
// reference on the entry. The only ways that this can become false without the
// entry being passed to its "deleter" are via Erase(), via Insert() when
// an element with a duplicate key is inserted, or on destruction of the cache.
//
// The cache keeps two linked lists of items in the cache. All items in the
// cache are in one list or the other, and never both. Items still referenced
// by clients but erased from the cache are in neither list. The lists are:
// - in-use: contains the items currently referenced by clients, in no
// particular order. (This list is used for invariant checking. If we
// removed the check, elements that would otherwise be on this list could be
// left as disconnected singleton lists.)
// - LRU: contains the items not currently referenced by clients, in LRU order
// Elements are moved between these lists by the Ref() and Unref() methods,
// when they detect an element in the cache acquiring or losing its only
// external reference.

// An entry is a variable length heap-allocated structure. Entries
// are kept in a circular doubly linked list ordered by access time.
// 每一个缓存项都保存在一个LRUHandler里
// 即每个资源都是一个handler
// 每个handler是变长的堆分配内存
// handlers会被放置在两个双向循环链表中


struct LRUHandle
{
void *value;
// 数据项被移出缓存时的回调函数,这是一个函数指针
void (*deleter)(const Slice &, void *value);
LRUHandle *next_hash; // hashtable中下一个handler
LRUHandle *next; // 双向链表中下一个
LRUHandle *prev; // 双向链表中前一个
size_t charge; // 缓存项的大小
size_t key_length;
bool in_cache; // 是否位于缓存中
uint32_t refs; // 引用计数
uint32_t hash; // key()的哈希值,用于快速查找
char key_data[1]; // key的开始位置

Slice key() const
{
// 只有当链表为空时头指针的next才会指向本身,这是毫无意义的
assert(next != this);
return Slice(key_data, key_length);
}
};

}

其位于一个匿名名称空间中,而且这个结构体长得比较奇怪。还记得前面提到过,每个handler同时存在于一个循环双向链表中和一个哈希表中,所以在一个结构体内存在这么多指针就是为了标识当前handler在两个表中的位置。话说这个结构体是不是没有内存对齐...

HandleTable

1
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

namespace // 同样是匿名名称空间
{
// 这里定义HandleTable,其实就是hashtable,因为这会比自带的hashtable快
class HandleTable
{
public:
HandleTable() : length_(0), elems_(0), list_(nullptr) { Resize(); }
~HandleTable() { delete[] list_; }

// 查找一个key并返回其handler
LRUHandle *Lookup(const Slice &key, uint32_t hash)
{
return *FindPointer(key, hash);
}

// 插入一个handler到hashtable中,这部分如果看不懂建议去看看hashtable原理
// insert和remove不涉及内存管理!
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;
if (old == nullptr)
{
++elems_;
if (elems_ > length_)
{
// Since each cache entry is fairly large, we aim for a small
// average linked list length (<= 1).
// 如果元素数目比hash长度还要大,就扩容,因为这样hash冲突比较显著
Resize();
}
}
return old;
}

// 移除一个handler
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;
}

private:
uint32_t length_; // hash长度
uint32_t elems_; // 数目
LRUHandle **list_; //指向存放handler*的数组 用于hash冲突

// 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和key来查找handler,因为hash冲突存在,必须要保留key用于验证
// 如果key不存在,则返回对应的hashtable表项中的最后一个
// 但是我感觉返回的是null,即超尾元素
LRUHandle **FindPointer(const Slice &key, uint32_t hash)
{
LRUHandle **ptr = &list_[hash & (length_ - 1)];
while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key()))
{
ptr = &(*ptr)->next_hash;
}
return ptr;
}

// 重新调整长度,为了减弱hash冲突
void Resize()
{
uint32_t new_length = 4;
while (new_length < elems_)
{
new_length *= 2; // 每次扩大2倍的方式移动
}
// 然后新建一个list并把原来的各个表项进行重映射
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;
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);
// 然后删除掉原来的list
delete[] list_;
list_ = new_list;
length_ = new_length;
}
};
}

这个HandleTable也是位于一个匿名的名称空间中。它其实就是一个HashTable,至于为什么要再实现一个HashTable,它也在注释中说了,因为新实现的这个会更快(gcc -O3相比提升5%左右)。

由于其本身就是一个HashTable了,所以没有什么好说的,本质上还是很简单的。可能唯一要注意的就是元素数目和hash的桶数组长度相同时的扩容操作吧,总感觉这个扩容的函数有点暴力美学了,感觉应该还可以继续优化?

另外,HandleTable不涉及内存管理的操作。

LRUCache

这个LRUCache呢,就可以说是一个实现的比较完整的Cache了。它内部使用了HandleTable、LRUHandle,提供了完整的增删查缓存的能力。但是需要注意的是,这个类没有继承自Cache类,所以它不能使用Cache指针来实现多态。

但实际上,我们没有办法创建一个LRUCache类,因为其位于一个匿名名称空间,且没有提供外部接口来访问它,所以它本质上仍然是不完全体的Cache。

1
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238

namespace // 位于匿名名称空间
{
class LRUCache
{
public:
// 构造析构函数
LRUCache();
~LRUCache();

// 设置容量大小
void SetCapacity(size_t capacity) { capacity_ = capacity; }

// 插入key/hash/value,并返回其handler指针,同时为其增加deleter回调函数
// 与Cache::Insert类似,只是多了一个hash
Cache::Handle *Insert(const Slice &key, uint32_t hash, void *value,
size_t charge,
void (*deleter)(const Slice &key, void *value));
// 查找一个key/hash是否在缓存中,如果在就返回其handler指针
Cache::Handle *Lookup(const Slice &key, uint32_t hash);
void Release(Cache::Handle *handle); // 调用完成后释放
void Erase(const Slice &key, uint32_t hash); // 当引用归零后移除
void Prune(); // 移除不使用的缓存以释放空间
size_t TotalCharge() const // 总的使用空间
{
MutexLock l(&mutex_);
return usage_;
}

private:
void LRU_Remove(LRUHandle *e); // 移除handler
void LRU_Append(LRUHandle *list, LRUHandle *e); // 添加handler
void Ref(LRUHandle *e); // 对handler进行引用计数的俩函数
void Unref(LRUHandle *e);
// 完成了移除的收尾工作的函数
bool FinishErase(LRUHandle *e) EXCLUSIVE_LOCKS_REQUIRED(mutex_);

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_); // 在LRUCache中但未被引用的缓存项链表头哑结点

// 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)
{
// 构造一个空的循环双向链表
lru_.next = &lru_;
lru_.prev = &lru_;
in_use_.next = &in_use_;
in_use_.prev = &in_use_;
}

// 析构函数
LRUCache::~LRUCache()
{
// 先判断一下本Cache中in_use中是否没有数据了,即保证所有的项都没有被使用中
assert(in_use_.next == &in_use_);
for (LRUHandle *e = lru_.next; e != &lru_;)
{
LRUHandle *next = e->next;
assert(e->in_cache);
e->in_cache = false;
assert(e->refs == 1); // 只有LURCache引用了它
Unref(e); // 释放掉
e = next;
}
}

// 增加引用计数
void LRUCache::Ref(LRUHandle *e)
{
if (e->refs == 1 && e->in_cache)
{
// 如果在lru_list中,就移动到in_use中
LRU_Remove(e);
LRU_Append(&in_use_, e);
}
e->refs++; // 增加引用计数
}

// 减少引用计数
void LRUCache::Unref(LRUHandle *e)
{
assert(e->refs > 0);
e->refs--;
if (e->refs == 0)
{
// 没有东西在引用他,就直接销毁,调用其deleter
assert(!e->in_cache);
(*e->deleter)(e->key(), e->value);
free(e);
}
else if (e->in_cache && e->refs == 1)
{
// 不在使用就移动至lru_list中
LRU_Remove(e);
LRU_Append(&lru_, e);
}
}

// 移除一个handler
// 并不是销毁,主要用于在两个链表之间移动handler
void LRUCache::LRU_Remove(LRUHandle *e)
{
e->next->prev = e->prev;
e->prev->next = e->next;
}

// 添加到指定的链表中,同样是移动操作
void LRUCache::LRU_Append(LRUHandle *list, LRUHandle *e)
{
// 按照头插入法进行插入
// 保证prev是最新的,next是最旧的
e->next = list;
e->prev = list->prev;
e->prev->next = e;
e->next->prev = e;
}

// 查找操作
Cache::Handle *LRUCache::Lookup(const Slice &key, uint32_t hash)
{
MutexLock l(&mutex_); // 锁
LRUHandle *e = table_.Lookup(key, hash);
if (e != nullptr)
{
Ref(e); // 查找到就移动至in_usr并增加引用计数
}
return reinterpret_cast<Cache::Handle *>(e);
}

// 释放操作
void LRUCache::Release(Cache::Handle *handle)
{
MutexLock l(&mutex_); // 释放锁
Unref(reinterpret_cast<LRUHandle *>(handle));
}

// 插入操作
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
{
// 没有容量就表示不缓存,所以自然也不会位于缓存表中
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);
}

// 完成了移除的收尾工作的函数
// 从lru中移除,并保证其引用计数归零
bool LRUCache::FinishErase(LRUHandle *e)
{
if (e != nullptr)
{
assert(e->in_cache);
LRU_Remove(e);
e->in_cache = false;
usage_ -= e->charge;
Unref(e);
}
return e != nullptr;
}

// 其实就是加锁然后从hashtable中移除,并调用FinishErase彻底移除
void LRUCache::Erase(const Slice &key, uint32_t hash)
{
MutexLock l(&mutex_);
FinishErase(table_.Remove(key, hash));
}

// 释放空间,跟Insert里面那个差不多
void LRUCache::Prune()
{
MutexLock l(&mutex_);
while (lru_.next != &lru_)
{
LRUHandle *e = lru_.next;
assert(e->refs == 1);
bool erased = FinishErase(table_.Remove(e->key(), e->hash));
if (!erased)
{ // to avoid unused variable when compiled NDEBUG
assert(erased);
}
}
}
}

LRUCache类实现的已经比较完整了。需要注意的是,其使用两个双向链表存储handler,每个handler只会位于一个双向链表中。且由于双向链表始终是头插入,所以head->prev就是最新进入链表的,head->next就是最老进入链表的。当缓存空间占用超过限制时,可以从head->next开始清理空间。

另外,这个类中还涉及到了很多锁的概念,因为我不是特别清楚,只是大致了解锁的作用,所以就浅浅掠过吧。

ShardedLRUCache

如果说LRUCache是一个相对完整的半成品Cache,那么ShardedLRUCache就是完成品了。虽然它也位于一个匿名名称空间中,但是其对外提供了创建接口。

它相对于LRUCache的主要改进是:由于LRUCache使用锁导致多线程时性能下降,所以ShardedLRUCache采用一些方案来弥补锁带来的阻塞。方法是将一个大的Cache分成若干个小的Cache,然后每个线程固定访问一个Cache,这样就算有锁,也只会在每个小Cache上阻塞,大大提升了并发能力。

这个ShardedLRUCache继承自Cache,所以有Cache的全部接口。

1
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
88
89
90
91
92

namespace // 匿名空间
{
class ShardedLRUCache : public Cache
{
private:
LRUCache shard_[kNumShards];
port::Mutex id_mutex_;
uint64_t last_id_;

// 计算hash值的函数
static inline uint32_t HashSlice(const Slice &s)
{
return Hash(s.data(), s.size(), 0);
}

// 截取分片
static uint32_t Shard(uint32_t hash) { return hash >> (32 - kNumShardBits); }

public:
// 构造函数,为每个Cache设置大小
explicit ShardedLRUCache(size_t capacity) : last_id_(0)
{
const size_t per_shard = (capacity + (kNumShards - 1)) / kNumShards;
for (int s = 0; s < kNumShards; s++)
{
shard_[s].SetCapacity(per_shard);
}
}
// 析构函数,为空
~ShardedLRUCache() override {}
// 插入一个key到缓存中
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);
}
// 释放handler
void Release(Handle *handle) override
{
LRUHandle *h = reinterpret_cast<LRUHandle *>(handle);
shard_[Shard(h->hash)].Release(handle);
}
// 移除
void Erase(const Slice &key) override
{
const uint32_t hash = HashSlice(key);
shard_[Shard(hash)].Erase(key, hash);
}
// 返回值
void *Value(Handle *handle) override
{
return reinterpret_cast<LRUHandle *>(handle)->value;
}
// 创建一个id
uint64_t NewId() override
{
MutexLock l(&id_mutex_);
return ++(last_id_);
}
// 清理空间
void Prune() override
{
for (int s = 0; s < kNumShards; s++)
{
shard_[s].Prune();
}
}
// 总占用
size_t TotalCharge() const override
{
size_t total = 0;
for (int s = 0; s < kNumShards; s++)
{
total += shard_[s].TotalCharge();
}
return total;
}
};

} // end anonymous namespace

// 外部接口
Cache *NewLRUCache(size_t capacity) { return new ShardedLRUCache(capacity); }

这个类主要就是使用LRUCache,由于LRUCache中实现已经很完善了,所以这个类就相对简单一些,主要特性就是上面描述的那些。

缓存的使用

TableCache

LevelDB里SSTable在内存中是以Table结构存在的,要使用一个SSTable,必须先进行Open操作,会将Index Block和Filter Data都读取到内存里,保存在Table里,但是Data Block依然保存在磁盘上。在缓存的时候也只会缓存索引,和指向数据的迭代器,在TableCache中不会缓存数据本身。

  • 每个Table结构都要占据一定的内存,被打开的Table放在一个缓存中,缓存一定数量的Table,当数量太多时,有一些Table需要被驱逐出内存,这样当需要再次访问这些Table时需要再次被打开
  • 每个Table的Data Block可以被缓存,这样再次访问相同的数据时,不需要读磁盘

我们只关注一下TableCache类的关键的几个函数

1
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

struct TableAndFile
{
RandomAccessFile *file;
Table *table;
};

// 删除一个handler时的回调函数
// 用于管理内存的
static void DeleteEntry(const Slice &key, void *value)
{
TableAndFile *tf = reinterpret_cast<TableAndFile *>(value);
delete tf->table;
delete tf->file;
delete tf;
}

// 从TableCache中查找Table缓存的函数
Status TableCache::FindTable(uint64_t file_number, uint64_t file_size,
Cache::Handle **handle)
{
Status s;
char buf[sizeof(file_number)];
EncodeFixed64(buf, file_number);
Slice key(buf, sizeof(buf)); // 查找时的key为filenumber的编码
*handle = cache_->Lookup(key);
if (*handle == nullptr)
{
std::string fname = TableFileName(dbname_, file_number);
RandomAccessFile *file = nullptr;
Table *table = nullptr;
s = env_->NewRandomAccessFile(fname, &file);
if (!s.ok())
{
std::string old_fname = SSTTableFileName(dbname_, file_number);
if (env_->NewRandomAccessFile(old_fname, &file).ok())
{
s = Status::OK();
}
}
if (s.ok())
{
s = Table::Open(options_, file, file_size, &table);
}

if (!s.ok())
{
assert(table == nullptr);
delete file;
// We do not cache error results so that if the error is transient,
// or somebody repairs the file, we recover automatically.
}
else
{
TableAndFile *tf = new TableAndFile;
tf->file = file;
tf->table = table;
*handle = cache_->Insert(key, tf, 1, &DeleteEntry);
}
}
return s;
}

这个函数的作用是查找一个Table,我们注意到,查找的时候使用的是Table的file_number。即Table就是用这个file_number作为键来缓存的。用一个结构体缓存了Table类和一个文件读取迭代器,并把这个结构体插入到TableCache类中的hardedLRUCache对象中。

BlockCache

之前也提到过,TableCache是不会缓存具体的数据而是缓存索引,而BlcokCache的存在就是为了缓存数据,即缓存DataBlock。

每个Table打开的时候,都会指定一个cache_id,这是一个单调递增的整数,每个Table都有一个唯一的cache_id。在每一个SSTable里面,每一个Data Block都有一个固定的文件偏移offset。所以每一个Data Block都可以由cache_id和offset来唯一标识,也就是根据这两个值生成一个键,来插入和查找缓存。

BlockCache被Table调用,当读取block时会优先从缓存中读取,如果缓存中没有就需要从文件中读取,然后将其放到缓存上。其中BlockCache的缓存的key是由cache_id和block的offset共同组成。

1
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
88
89
90

// 删除BlcokCache的回调函数
static void DeleteCachedBlock(const Slice &key, void *value)
{
Block *block = reinterpret_cast<Block *>(value);
delete block;
}

// Convert an index iterator value (i.e., an encoded BlockHandle)
// into an iterator over the contents of the corresponding block.
Iterator *Table::BlockReader(void *arg, const ReadOptions &options,
const Slice &index_value)
{
Table *table = reinterpret_cast<Table *>(arg);
Cache *block_cache = table->rep_->options.block_cache;
Block *block = nullptr;
Cache::Handle *cache_handle = nullptr;

BlockHandle handle;
Slice input = index_value;
Status s = handle.DecodeFrom(&input);
// We intentionally allow extra stuff in index_value so that we
// can add more features in the future.

if (s.ok())
{
BlockContents contents;
// BlockCache在这里
// 根据block handle,首先尝试从blockcache中直接取出block,不在blockcache中则
// 调用ReadBlock从文件读取,读取成功后,根据option尝试将block加入到blockcache中。
// 并在Insert的时候注册了释放函数DeleteCachedBlock。
if (block_cache != nullptr)
{
char cache_key_buffer[16];
EncodeFixed64(cache_key_buffer, table->rep_->cache_id);
EncodeFixed64(cache_key_buffer + 8, handle.offset());
Slice key(cache_key_buffer, sizeof(cache_key_buffer));
// 先查一下缓存,key是由cache_id和handler的偏移值共同组成
cache_handle = block_cache->Lookup(key);
if (cache_handle != nullptr)
{
// 查找到就优先从缓存中返回
block = reinterpret_cast<Block *>(block_cache->Value(cache_handle));
}
else
{
// 否则就添加到缓存
s = ReadBlock(table->rep_->file, options, handle, &contents);
if (s.ok())
{
block = new Block(contents);
if (contents.cachable && options.fill_cache)
{
cache_handle = block_cache->Insert(key, block, block->size(),
&DeleteCachedBlock);
}
}
}
}
else
{
s = ReadBlock(table->rep_->file, options, handle, &contents);
if (s.ok())
{
block = new Block(contents);
}
}
}

// 返回block的迭代器
Iterator *iter;
if (block != nullptr)
{
iter = block->NewIterator(table->rep_->options.comparator);
if (cache_handle == nullptr)
{
iter->RegisterCleanup(&DeleteBlock, block, nullptr);
}
else
{
iter->RegisterCleanup(&ReleaseBlock, block_cache, cache_handle);
}
}
else
{
iter = NewErrorIterator(s);
}
return iter;
}

缓存使用上的总结

levelDB默认提供了一个完整的可用缓存为ShardedLRUCache,后续所有的缓存使用都是基于此类来实现的。这个类继承自Cache类,拥有所有的接口。

TableCache类缓存索引项,是Table类的一个友元。而Table类用于读取一个被持久到文件的sstable,所以Table类存在xxxblock和footer等。Table在不缓存的条件下必须经过磁盘来读取DataBlock

BlockCache实际上是Table内部的组成,用于缓存DataBlock。

查找某个Table中的某一条数据的流程如下:

  • 将Table的file_number作为键,通过TableCache类来查找这个Table。如果存在就返回,否则就需要调用Table::Open()函数来从文件中读取它,并放入缓存。
  • 拿到Table后,先通过其索引来确定这条数据位于哪个Block。使用cache_id和offset来构造键,通过全局的一个BlockCache(定义在options中),来查找这个Block,如果找到就返回,否则就从文件打开并添加到缓存中。

TableCache被定义成了Table的友元类是因为索引项比较多,且索引都存储在Table中。

BlockCache甚至都不存在这样一个类,是因为Block比较简单,本质上就是一个数组,能够直接用于Cache。

另外一方面的原因就是,xxx类的缓存会包含很多个xxx类。Table与Block刚好构成这样一个关系。

总结

  • 使用匿名名称空间来隐藏类
  • 使用override来显示指示出函数需要重写
  • 使用回调函数来管理内存