目录
整体说来,rocksdb对于LRUCache的实现还是比较简单的,和我们平时见到的LRUCache基本一致,核心数据结构包括一个hashtable,用于存放cache所管理的数据,另一个数据结构为一个由双向循环链表实现的LRUList, 用于提供LRU语义。除了LRUCache以外,rocksdb还提供了另外几种Cache实现,LRUCache在rocksdb的Cache继承体系如下所示:
LRUHandle
LRUHandle是LRUCache存储的最基本元素。该对象用于封装上层调用者传来的value和key,另外需要注意的是被LRUCache管理的数据都应该在堆上分配。LRUHandle的关键数据如下:
struct LRUHandle {
/* 实际的value */
void* value;
/* 析构函数 */
void (*deleter)(const Slice&, void* value);
/* 用于hashtable,拉链法的下一个元素 */
LRUHandle* next_hash;
/* 用于LRU链表 */
LRUHandle* next;
LRUHandle* prev;
size_t charge;
size_t key_length;
uint32_t refs; // a number of refs to this entry
// cache itself is counted as 1
/*
* 记录了该Handle是否在cache中。
* 该Handle是否为高优先级Handle,由调用者插数据时指定。
* 是否位于高优先级队列中,如果该handle为高优先级Handle,则会将其插入LRU链表。
*/
char flags;
uint32_t hash; // Hash of key(); used for fast sharding and comparisons
/* 真正的key数据: key_data[0] -- key_data(key_length) */
char key_data[1]; // Beginning of key
}
LRUHandle共有三种状态:
- 被外部引用并且在LRUCache的hashtable中, 需要注意的是如果一个Handle被外部引用,那么rocksdb就不会将该元素放在LRU链表中。此时Handle的引用计数应该大于1,并且in_cache == true。
- 没有被外部引用,此时该Handle会被LRU链表管理,在内存不足时可以释放掉。此时Handle的引用计数等于1,并且in_cache == true。
- 被外部引用,但是不在cache中,此时Handle的引用计数大于0,并且in_cache == false。
状态转换流程如下:
- 当想LRUCache中插入Handle时,此时Handle的状态为state1
- 在state1的基础上,对一个Handle执行Release操作,该Handle的状态将为state2
- 在state1的基础上,对一个Handle执行Erase操作,该Handle的状态将为state3
- 在state2的基础上,如果caller查到到一个Handle,此时状态将为state1
另外需要注意的是,对于rocksdb的LRUCache的实现,Handle在hashtable中不一定在LRU链表中,但是Handle在LRU链表中,一定在hashtable中。
LRUCache
整体上管理LRUCache的类。为了减少锁冲突,rockdb将一个LRUCache分割成一系列小的LRUCache分片,每一个LRUCache分片用LRUCacheShard对象表示。所以LRUCache类拥有一个LRUCacheShared列表。整体说来LRUCache并没有对于LRU-Cache管理的核心逻辑,类接口都是一些辅助函数,其类定义如下:
class LRUCache : public ShardedCache {
public:
// 构造函数会根据num_shard_bits创建一系列LRUCacheShard对象
LRUCache(size_t capacity, int num_shard_bits, bool strict_capacity_limit,
double high_pri_pool_ratio);
virtual ~LRUCache();
virtual const char* Name() const override { return "LRUCache"; }
virtual CacheShard* GetShard(int shard) override;
virtual const CacheShard* GetShard(int shard) const override;
virtual void* Value(Handle* handle) override;
virtual size_t GetCharge(Handle* handle) const override;
virtual uint32_t GetHash(Handle* handle) const override;
virtual void DisownData() override;
private:
LRUCacheShard* shards_;
};
LRUCacheShard
LRUCacheShard代表一个LRU-Cache的分片,该类实现了LRU-Cache的核心语义。首先需要确定一点,LRUCacheShard管理的所有数据都被存放在了一个hashtable中,该类为LRUHandleTable。LRUHandleTable是rocksdb自己实现的hashtable类,其提供的语义与常见的hashtable的语义相同,但是该类具有比系统类库更好的性能。另外,LRUCacheShard还持有一个双向循环链表,用于实现LRU语义。
一般来说,如果一个数据空间加入到LRUCache后,该数据空间不但被hashtable引用,还会被LRU链表引用。但是rocksdb实现的LRUCache语义与常见的LRUCache有一点不同:
- hashtable持有LRUCache所有的数据
- 当一个Handle不被外部引用时,它会被LRU链表引用,表示可回收
- 当cache的内存不足时,先回收LRU链表引用数据的内存
下面一张图展示了hashtable管理的内存和LRU链表管理的内存之间的关系:
LRUCacheShard关键类成员如下:
class LRUCacheShard : public CacheShard {
public:
LRUCacheShard();
virtual ~LRUCacheShard();
/* 向LRUCache中插入数据 */
virtual Status Insert(const Slice& key, uint32_t hash, void* value,
size_t charge,
void (*deleter)(const Slice& key, void* value),
Cache::Handle** handle,
Cache::Priority priority) override;
/* 从LRUCache中查找数据 */
virtual Cache::Handle* Lookup(const Slice& key, uint32_t hash) override;
/* 解引用一个Handle,视根据内存的使用情况和Handle的引用计数而定,该Handle不一定会被在cache中抹除 */
virtual bool Release(Cache::Handle* handle,
bool force_erase = false) override;
/* 从LRUCache中抹除 */
virtual void Erase(const Slice& key, uint32_t hash) override;
private:
void LRU_Remove(LRUHandle* e);
void LRU_Insert(LRUHandle* e);
/* 当高优先级链表引用的数据超过一个阈值时,将高优先级链表引用的数据,调整到低优先级链表上 */
void MaintainPoolSize();
void EvictFromLRU(size_t charge, autovector<LRUHandle*>* deleted);
/*
* LRUCahche管理的内存上限。
* 以下几个关于LRUCache的内存相关的数据指标,都仅仅只包括caller传入的charge,
* 不包括LRUCache自身数据结构占用的内存
*/
size_t capacity_;
/* 所有驻留在hashtable中的元素所占的内存大小 */
size_t usage_;
/* LRUList管理的内存大小 */
size_t lru_usage_;
/* 高优先级LRU链表管理的内存大小 */
size_t high_pri_pool_usage_;
/* 开启严格模式后,内存超限,则报错 */
bool strict_capacity_limit_;
/* 高优先级LRU链表能够管理的内存最大大小 */
double high_pri_pool_capacity_;
mutable port::Mutex mutex_;
/* Dummy head of LRU list. */
LRUHandle lru_;
/* 低优先链表的链表头 */
LRUHandle* lru_low_pri_;
LRUHandleTable table_;
};
关于LRUCacheShard不同优先级链表的实现
上文中提到过,rocksdb的LRUCache是通过一个双向循环链表来实现LRU语义的,该循环链表有一个dummy的链表头lru_,链表的元素为LRUHandle,LRUHandle为LRUCache管理的最基本的元素,该对象用于封装上层调用者传来的value和key。
LRUCache有一个成员变量lru_low_pri_,用于指向低优先级的队列头。初始时LRU队列为空,每次有新元素插入时,对于高优先级元素会将其插入队列尾部,对于低优先级元素会将其插入低优先级队列的头部。比如,我们先插两个低优先级的元素,再插两个高优先级的元素,LRU链表的结构应该是这样的:
接着再次插入一个低优先级的元素:
如果LRU队列设置的高优先级的链表长度最多为2,那么我们再次插入一个高优先级元素后: