leveldb深度剖析-TableCache

TableCache设计的出发点就是:提升性能。根据著名的局部性访问原理,leveldb设计了一个简单LRUCache算法,该算法是TableCache的核心,下面我们就来分析一下leveldb是如何实现的。

一、TableCache

先来看一下TableCache的类定义,非常简洁:

class TableCache {
 public:
  TableCache(const std::string& dbname, const Options* options, int entries);
  ~TableCache();
  //迭代器
  Iterator* NewIterator(const ReadOptions& options,
                        uint64_t file_number,
                        uint64_t file_size,
                        Table** tableptr = NULL);
  // 查询接口
  Status Get(const ReadOptions& options,
             uint64_t file_number,
             uint64_t file_size,
             const Slice& k,
             void* arg,
             void (*handle_result)(void*, const Slice&, const Slice&));

  // Evict any entry for the specified file number
  // 根据文件编号 从cache中删除指定项
  void Evict(uint64_t file_number);

 private:
  Env* const env_;
  const std::string dbname_;
  const Options* options_;
  Cache* cache_; 封装LRUCache 在构造函数中初始化
  //根据文件编号 查找文件
  Status FindTable(uint64_t file_number, uint64_t file_size, Cache::Handle**);
};

1.1、查询接口

/**
 * 从ldb中获取数据
 * @param options 选项
 * @param file_number ldb文件编号  来自FileMetaData
 * @param file_size   ldb文件大小  来自FileMetaData
 * @param k           查找key值
 * @param arg         回调函数参数
 * @param saver       回调函数
 */
Status TableCache::Get(const ReadOptions& options,
                       uint64_t file_number,
                       uint64_t file_size,
                       const Slice& k,
                       void* arg,
                       void (*saver)(void*, const Slice&, const Slice&)) {
  Cache::Handle* handle = NULL;
  Status s = FindTable(file_number, file_size, &handle);// 打开ldb文件并且读取出data index block以及meta index block
  if (s.ok()) {
    Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;
    s = t->InternalGet(options, k, arg, saver);// 在table中查找k 如果找到则通过saver回调函数进行保存
    cache_->Release(handle);//必须调用
  }
  return s;
}

这里强调一下:必须调用cache_->Release(handle)接口,查询接口十分的简单,代码注释也很清楚,下面来看一下FindTable具体实现 。

1.2、FindTable 

/**
 * 查找Table
 * @param file_number ldb文件编号  来自FileMetaData
 * @param file_size   ldb文件大小  来自FileMetaData
 * @param handle      cache handle对象 输出参数
 */
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));
  *handle = cache_->Lookup(key);//按照key进行查找 如果没有找到则说明是新文件
  if (*handle == NULL) {
    std::string fname = TableFileName(dbname_, file_number);//读取ldb文件
    RandomAccessFile* file = NULL;
    Table* table = NULL;
    s = env_->NewRandomAccessFile(fname, &file);
    if (!s.ok()) {// 兼容 1.13版本(含)之前是sst文件作为后缀
      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);//打开文件 创建table对象
    }

    if (!s.ok()) {
      assert(table == NULL);
      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;//对文件对象和Table对象进行包装
      tf->file = file;
      tf->table = table;//保存table对象
      
      //插入cache中 默认返回LRUHandle对象 此处key是文件编号
      *handle = cache_->Insert(key, tf, 1, &DeleteEntry);
    }
  }
  return s;
}

说明:

1)Cache 缓存的实际是ldb文件对象以及Table,可以通过Cache::Insert看出来。那么查询关键字是什么呢?是文件编号,也就是文件名字中的数字。

2)如果通过cache->Lookup没有找到说明是新文件,那么就要执行open操作,对ldb文件进行解析,这个解析过程是无法避免的而且是比较耗时的,这也就是为什么leveldb设计了一个LRUCache的原因(根据局部性原理)。

3)上面Table::Open实际是读取ldb文件并对其解析,例如解析出foot,index block,data block等,这部分是最好费性能的。

4)将file和table这两个对象封装到TableAndFile对象中并且将其插入到cache中。

二、ShardedLRUCache

类Cache只是接口,真正实现类为ShardedLRUCache,这部分内容比较凌乱,涉及到一些类,我对其进行了总结:

类名称说明备注
类Cache抽象类,几乎所有接口都是虚函数 
类ShardedLRUCache继承Cache,实现类 

类LRUCache

ShardedLRUCache定义了16分片,每个分片类型为LRUCache每个LRU是独立的,线程安全的,可以提升并发访问
结构体Handle空结构体,用于接口。内部实际为LRUHandle 
结构体LRUHandle每个LRUHandle相当于cache中一个元素cache组织方式是双向链表,LRUHandle相当于链表中一个元素
类HandleTabble为了提升性能,leveldb还增加了HashTable用于存储LRUHandle 

由于SharedLRUCache方法基本上是对LRUCache方法的封装,所以这里只罗列两个方法,后面在介绍LRUCache的时候在详细介绍。

static const int kNumShardBits = 4;
static const int kNumShards = 1 << kNumShardBits;//16  
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);//每个分片cache的容量为per_shard,超过这个就需要移除旧元素
  }
}
  virtual Handle* Insert(const Slice& key, void* value, size_t charge,
                         void (*deleter)(const Slice& key, void* value)) {
    const uint32_t hash = HashSlice(key);//对key进行hash 然后插入到对应的cache中
    return shard_[Shard(hash)].Insert(key, hash, value, charge, deleter);
  }

三、结构体LRUHandle

该结构体比较简单,注释已经给出详细说明。

struct LRUHandle {
  void* value; //value
  void (*deleter)(const Slice&, void* value);
  LRUHandle* next_hash;// 在hashtable中使用 

  LRUHandle* next; //双向链表
  LRUHandle* prev; //双向链表

  size_t charge;      // 占用cache空间数目,目前始终为1
  size_t key_length;
  bool in_cache;      // 表示当前元素是否在cache中 false表示回收内存.
  uint32_t refs;      // 当引用计数为0时就要删除 
  uint32_t hash;      // Hash of key(); used for fast sharding and comparisons
  char key_data[1];   // key值

  Slice key() const {
    // For cheaper lookups, we allow a temporary Handle object
    // to store a pointer to a key in "value".
    if (next == this) {
      return *(reinterpret_cast<Slice*>(value));
    } else {
      return Slice(key_data, key_length);
    }
  }
};

四、LRUCache

下面来看一下LRUCache定义以及相关内容介绍。

// A single shard of sharded cache.
class LRUCache {
 public:
  LRUCache();
  ~LRUCache();

  // Separate from constructor so caller can easily make an array of LRUCache
  // 默认值是62
  void SetCapacity(size_t capacity) { capacity_ = capacity; }

  // Like Cache methods, but with an extra "hash" parameter.
  Cache::Handle* Insert(const Slice& key, uint32_t hash,
                        void* value, size_t charge,
                        void (*deleter)(const Slice& key, void* value));
  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);
  void LRU_Append(LRUHandle*list, LRUHandle* e);
  void Ref(LRUHandle* e);
  void Unref(LRUHandle* e);
  bool FinishErase(LRUHandle* e);

  // Initialized before use. 默认值是62
  size_t capacity_; //容量 超过这个容量后就要移除旧数据

  // mutex_ protects the following state.
  mutable port::Mutex mutex_;
  size_t usage_; //当前cache使用量

  // Dummy head of LRU list.
  // lru.prev is newest entry, lru.next is oldest entry.
  // Entries have refs==1 and in_cache==true.
  // 稳定状态下 所有元素都存在这个链表中 这个链表中元素refs一定等于1
  LRUHandle lru_;

  // Dummy head of in-use list.
  // Entries are in use by clients, and have refs >= 2 and in_cache==true.
  // 当用户查询某条记录时 会将元素从lru_移动到这个链表中  这个链表中元素refs一定大于等于2  当使用完毕后
  // refs自减 然后将元素移回到lru_中
  LRUHandle in_use_;

  HandleTable table_; /* 元素始终存在hashtable 只有从LRUCache删除时才会吧hashtable中元素删除 */
};

说明:

1)每个LRUCache都有一定容量,当存储的容量(usage_)超过阈值(capacity_)后就需要删除旧数据。这一点就是LRU思想。

2)LRUCache有两个链表和一个HashTable(请看注释内容),分别为:in_use_,lru_,table_

名称作用
in_use_所有的新元素都会放到这个链表中
lru_从in_use_中移除的元素会暂时保存在lru_中
table_hashTable,所有元素都会存在LRUHandle,用于查询接口Lookup。

特别说明:

1)一个元素会存储到hashtable中并且会存储到某个链表中(两个链表中的一个)。

2)新元素一定先存储到in_use_链表中,满足某些条件后会将元素移植到lru链表中,具体看下面分析。

3)当链表元素从lru_中删除,同时需要从hashtable中删除掉(彻底删除了)。

 4.1、Insert插入

/**
 * 插入LRUCache中
 * @param key 关键字
 * @param hash hash值
 * @param value value值
 * @param charge 
 * @param deleter 回调函数
 */
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中
  LRUHandle* e = reinterpret_cast<LRUHandle*>(
      malloc(sizeof(LRUHandle)-1 + key.size()));
  e->value = value;
  e->deleter = deleter;
  e->charge = charge;//占用cache容量 目前始终为1
  e->key_length = key.size();
  e->hash = hash;
  e->in_cache = false;
  e->refs = 1;  // for the returned handle.
  memcpy(e->key_data, key.data(), key.size());

  if (capacity_ > 0) {
    e->refs++;  // for the cache's reference. 注意这里refs已经变成2
    e->in_cache = true;
    LRU_Append(&in_use_, e);//插入链表
    usage_ += charge; //统计使用量
    FinishErase(table_.Insert(e));//插入hashtable中
  } // else don't cache.  (Tests use capacity_==0 to turn off caching.)

  /* 删除LRU链表中所有节点 */
  while (usage_ > capacity_ && lru_.next != &lru_) {
    LRUHandle* old = lru_.next;
    assert(old->refs == 1);
    bool erased = FinishErase(table_.Remove(old->key(), old->hash));//先从hashtable中删除
    if (!erased) {  // to avoid unused variable when compiled NDEBUG
      assert(erased);
    }
  }

  return reinterpret_cast<Cache::Handle*>(e);
}

说明:

1)默认capacity一定是大于0的,所以新元素会插入到in_use_链表中。

2)table_.Insert是将新元素插入到hashTable中,但是当hashTable中有重复元素(hash值相同&&key值相同)就会把旧元素返回回来。旧元素作为FinishErase输入参数,执行后续流程。

3)LRUCache使用场景有两个地方:open ldb文件和存储data block时。这两中场景不太相同,在open ldb文件这种场景下table_.insert返回值一定是null,因为key是文件编号,leveldb不可能打开同一个文件两次。 但是对于读取data block的时候却有可能出现table_.insert返回非空,比如user-key更新操作,返回飞控。

4)while表示in_use_链表存储已经达到最大,这个时候需要对LRU链表进行回收释放。

4.2、FinishErase

// If e != NULL, finish removing *e from the cache; it has already been removed
// from the hash table.  Return whether e != NULL.  Requires mutex_ held.
bool LRUCache::FinishErase(LRUHandle* e) {
  if (e != NULL) {
    assert(e->in_cache);
    LRU_Remove(e);//从in_use_链表中删除
    e->in_cache = false;
    usage_ -= e->charge; //减小使用计数
    Unref(e);
  }
  return e != NULL;
}

4.3、引用计数器

/**
 * 将引用计数加1
 * @param e
 */
void LRUCache::Ref(LRUHandle* e) {
  if (e->refs == 1 && e->in_cache) {//进入这个分支表示e一定在lru_链表中If on lru_ list, move to in_use_ list
    LRU_Remove(e);
    LRU_Append(&in_use_, e);
  }
  e->refs++;
}

/**
 * 释放引用计数
 * @param e 元素
 * @说明:
 *   要么直接回收内存 要么将元素e从in_use_链表移动到lru_链表
 */
void LRUCache::Unref(LRUHandle* e) {
  assert(e->refs > 0);
  e->refs--;
  if (e->refs == 0) { // Deallocate. 表示没有引用 直接释放内存
    assert(!e->in_cache);
    (*e->deleter)(e->key(), e->value);
    free(e);
  } else if (e->in_cache && e->refs == 1) {
    //可参考FindTabe函数 会进入这个分支 No longer in use; move to lru_ list.
    LRU_Remove(e);
    LRU_Append(&lru_, e);//插入LRU链表中
  }
}

通常情况下,元素e一定在lru_链表中,只有需要使用元素e的时候才把元素e从lru_中移动到in_use_链表中,这里的Unref方法需要注意,这里是唯一的地方将元素e插入到lru链表中。 

五、总结

至此,leveldb的LRUCache相关内容介绍完毕,还剩下一部分方法没有介绍,大家可自行阅读。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
根据引用,你提到了pip show jsonpath的安装成功。根据这个引用,我可以确认jsonpath已经成功安装在你的Python环境中。 至于你提到的"windows python LevelDB安装 metadata-generation-failed",我没有找到相关的引用来回答这个问题。但是我可以给你一些建议来解决这个问题。 首先,确保你已经正确地安装了LevelDB以及与之相关的依赖包。你可以尝试使用pip install命令来安装LevelDB。如果出现metadata generation failed的错误,可能是由于网络问题或者软件包源的问题导致的。你可以尝试使用其他的软件包源或者使用代理来解决这个问题。 另外,你还可以尝试使用conda来安装LevelDB,它是一个流行的Python包管理器,可以管理Python包及其依赖项。你可以在Anaconda官方网站上找到更多关于conda的信息和安装指南。 最后,如果上述方法都没有解决你的问题,我建议你检查一下你的操作系统和Python版本是否与LevelDB兼容。有时候,特定的软件包可能只能在特定的操作系统或者Python版本上正常工作。 希望这些建议对你有帮助,如果你还有其他问题,请随时提问。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [python38安装jsonpath失败问题解决](https://blog.csdn.net/qq_27371025/article/details/125855179)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [The Log: What every software engineer should know about real-time data's unifying abstraction](https://blog.csdn.net/iloveu8780/article/details/80097101)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值