关于lsm:LSMTree-LevelDb之LRU缓存
LSM-Tree - LevelDb之LRU缓存引言LRU缓存在各种开源组件中都有应用的场景,经常用于做冷热数据和淘汰策略,应用LRU次要有三点。 第一点是实现非常简单。第二点是代码量自身也不错。最初波及数据结构十分经典。LevelDB对于LRU缓存实现算是比拟经典的案例,这一节来介绍它是如何应用LRU实现缓存的。 LeetCode 中有一道相应LRU缓存算法的题目,感兴趣能够做一做:lru-cache实践依据wiki的LRU缓存构造介绍,能够理解到缓存的根本淘汰策略过程,比方上面这张图的节点淘汰过程: 读取的程序为 A B C D E D F,缓存大小为 4,括号内的数字示意排序,数字越小越靠后,示意 Least recently. 依据箭头的读取程序,读取到E的时候,发现缓存曾经满了,这时会淘汰最早的一个A(0)。 接下来持续读取并且更新排序,倒数第二次中发现D是最大的,而B是最小的,当读取F退出缓存之后,发现缓存曾经是满的,此时发现绝对于A之后插入的数值B拜访次数最小,于是进行淘汰并且替换。 依据起码实用准则LRU 的实现须要两个数据结构: HashTable(哈希表): 用于实现O(1)的查找。List: 存储 Least recently 排序,用于旧数据的淘汰。LevelDB实现这里间接看LevelDB是如何利用这个数据结构的。 LevelDB的LRUCache设计有4个数据结构,是顺次递进的关系,别离是: LRUHandle(LRU节点,也就是LRUNode)HandleTable(哈希表)LRUCache(要害,缓存接口标准和实现)ShardedLRUCache(用于进步并发效率)整个LevelDB 的数据结构组成如下: 上面是相干接口定义: // 插入一个键值对(key,value)到缓存(cache)中,// 并从缓存总容量中减去该键值对所占额度(charge) // // 返回指向该键值对的句柄(handle),调用者在用完句柄后,// 须要调用 this->Release(handle) 进行开释//// 在键值对不再被应用时,键值对会被传入的 deleter 参数// 开释virtual Handle* Insert(const Slice& key, void* value, size_t charge, void (*deleter)(const Slice& key, void* value)) = 0;// 如果缓存中没有相应键(key),则返回 nullptr//// 否则返回指向对应键值对的句柄(Handle)。调用者用完句柄后,// 要记得调用 this->Release(handle) 进行开释virtual Handle* Lookup(const Slice& key) = 0;// 开释 Insert/Lookup 函数返回的句柄// 要求:该句柄没有被开释过,即不能屡次开释// 要求:该句柄必须是同一个实例返回的virtual void Release(Handle* handle) = 0;// 获取句柄中的值,类型为 void*(示意任意用户自定义类型)// 要求:该句柄没有被开释// 要求:该句柄必须由同一实例所返回virtual void* Value(Handle* handle) = 0;// 如果缓存中蕴含给定键所指向的条目,则删除之。// 须要留神的是,只有在所有持有该条目句柄都开释时,该条目所占空间才会真正被开释virtual void Erase(const Slice& key) = 0;// 返回一个自增的数值 id。当一个缓存实例由多个客户端共享时,// 为了防止多个客户端的键抵触,每个客户端可能想获取一个独有// 的 id,并将其作为键的前缀。相似于给每个客户端一个独自的命名空间。virtual uint64_t NewId() = 0;// 驱赶全副没有被应用的数据条目// 内存吃紧型的利用可能想利用此接口定期开释内存。// 基类中的 Prune 默认实现为空,但强烈建议所有子类自行实现。// 未来的版本可能会减少一个默认实现。virtual void Prune() {}// 返回以后缓存中所有数据条目所占容量总和的一个预估virtual size_t TotalCharge() const = 0;依据LevelDB接口定义,能够整顿出缓存解决了如下的需要: ...