RocksDB LRUCache

本文详细介绍了RocksDB中LRUCache的实现细节,包括LRUHandle的三种状态及其转换,LRUCache的整体管理和LRUCacheShard的内部结构,特别是LRU链表与内存管理的关系。此外,还探讨了LRUCacheShard中不同优先级链表的管理策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

LRUHandle

LRUCache

LRUCacheShard

关于LRUCacheShard不同优先级链表的实现

整体说来,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,那么我们再次插入一个高优先级元素后:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值