[LevelDB]Block系统内幕解析-数据块(Data Block)

134 阅读6分钟

Block的基本信息

在这里插入图片描述 Block 是组成SSTable文件中的基本单元,主要有以下类型

  1. 数据块(Data Block):存储实际的键值对数据,按键排序并使用前缀压缩减少空间占用。
  2. 过滤块(Filter Block):包含布隆过滤器,用于快速判断一个键是否可能存在,避免不必要的磁盘读取。
  3. 元数据块(Meta Block):存储关于SSTable文件的额外元数据信息,如统计数据或特定功能的配置。
  4. 元数据索引块(Metaindex Block):保存指向各个元数据块的索引,方便查找特定类型的元数据。
  5. 索引块(Index Block):存储数据块的索引信息,记录每个数据块的最大键和偏移量,用于定位特定键所在的数据块。

数据块(基本block)

数据块(Data Block)作为LevelDB块系统的基本单元

特性

在这里插入图片描述

  1. 前缀压缩: 优化存储空间
  2. 重启点: 优化访问效率

前缀压缩

前缀压缩是一种数据结构优化技术,通过存储共享相同前缀的字符串来减少存储空间。

例子

在这里插入图片描述 在示例中:

  1. 有几个单词:apple, applet, application, apples, apply
  2. 它们都共享前缀"a"和"pp"
  3. 通过前缀压缩,共同前缀只存储一次,然后为每个单词分别存储其余部分 优势:节省存储空间,可以加快查找速度。

重启点

重启点定义:完整存储键(不使用前缀压缩)的位置 默认设置:每16个键值对设置一个重启点 实现逻辑:当计数器达到重启间隔(16)时,记录当前位置为重启点,并重置计数器

例子

假设有100个键值对, restart_interval= 16 在这里插入图片描述

  1. 键1:完整存储 (重启点1)
  2. 键2-16:与前一个键使用前缀压缩
  3. 键17:完整存储 (重启点2)
  4. 键18-32:与前一个键使用前缀压缩 以此类推...
  5. 键97:完整存储 (重启点7)
  6. 键98-100:与前一个键使用前缀压缩

实现方式

具体实现方式有两个组件 在这里插入图片描述

  1. Block:提供数据访问的能力
  2. BlockBuilder: 提供写入磁盘/内存的能力

逻辑细节

数据块写入:BlockBulder

BlockBuilder的写入能力,主要在 tablebuilder 中的被调用,逻辑例子如下

void TableBuilder::Add(const Slice& key, const Slice& value) {
...
  if (r->filter_block != nullptr) {
    r->filter_block->AddKey(key);
  }
...
}

BlockBuilder的Add方法是核心方法, 具体流程可以查看下面的流程图 在这里插入图片描述

例子

在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

源代码

block 数据结构

  class Block {
   private:
    const char* data_;             // 实际数据内容
    size_t size_;                  // 数据大小
    uint32_t restart_offset_;      // 重启点偏移
    bool owned_;                   // 是否拥有数据所有权
  };
void BlockBuilder::Add(const Slice& key, const Slice& value) {
 Slice last_key_piece(last_key_);// 将成员变量 使用局部变量承接,为了提升代码运行的效率
 assert(!finished_);// 判断块是否已经完成构建 = 是否调用过 Finish()方法
 assert(counter_ <= options_->block_restart_interval);// 确保计数器不超过重启点间隔
 assert(buffer_.empty()  // No values yet? 
        || options_->comparator->Compare(key, last_key_piece) > 0);// 确保新key > 上一个key 如果缓存区为空就跳过此检查-- buffer
 size_t shared = 0;// 初始化共享前缀长度为0

 if (counter_ < options_->block_restart_interval) {// 如果当前计数点 < 重启点间隔(默认16)
 // 启用前缀压缩的逻辑
   const size_t min_length = std::min(last_key_piece.size(), key.size());
   while ((shared < min_length) && (last_key_piece[shared] == key[shared])) {
     shared++;
   }
 } else {
   // 重置重启点
   restarts_.push_back(buffer_.size());
   counter_ = 0;
 }
 const size_t non_shared = key.size() - shared;

 // Add "<shared><non_shared><value_size>" to buffer_
 // 首先对 一些必要信息进行32变长编码
 PutVarint32(&buffer_, shared);
 PutVarint32(&buffer_, non_shared);
 PutVarint32(&buffer_, value.size());

 // Add string delta to buffer_ followed by value
 buffer_.append(key.data() + shared, non_shared);
 buffer_.append(value.data(), value.size());

 // 更新last_key的状态, 只复制必要的字节,相对于 key.tostring()
 last_key_.resize(shared);
 last_key_.append(key.data() + shared, non_shared);
 assert(Slice(last_key_) == key);
 counter_++;
}

数据块读取:Block

目标代码:  leveldb/table/block.cc

Block主要对外提供以下功能,总的来说 提供对sstable/block的生命周期管理能力

  1. 通过内置的 iterator 来提供对数据的遍历和检索能力
  2. 通过BlockContents创建Block对象
对外接口调用示例
Status Table::InternalGet(const ReadOptions& options, const Slice& k, void* arg,
                          void (*handle_result)(void*, const Slice&,
                                                const Slice&)) {
...
      Iterator* block_iter = BlockReader(this, options, iiter->value());
      block_iter->Seek(k);
...
}

简单来所,读取包含目标键的数据块并在其中搜索目标键

  1. 先通过BlockReader函数读取并解析一个数据块(Block对象),然后创建该块的迭代器,
  2. 使用Seek(k)在块中查找与目标键k匹配的条目。

Block读取的特征如下

  1. 启发式的局部性优化
  2. 二分法查找优化
逻辑细节

block读取的流程如下图 在这里插入图片描述 接下来举个具体的例子来说明 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

源代码
    void Seek(const Slice& target) override {
    // 二分查找,找到最后一个重启点,使得key < target 
    uint32_t left = 0;
    uint32_t right = num_restarts_ - 1; 
    int current_key_compare = 0;

    // 对二分查找的启发形式优化, 相当于直接使用当前的状态信息,先进行一次优化
    if (Valid()) {
      // If we're already scanning, use the current position as a starting
      // point. This is beneficial if the key we're seeking to is ahead of the
      // current position.
      current_key_compare = Compare(key_, target);
      if (current_key_compare < 0) {
        // key_ is smaller than target
        left = restart_index_;
      } else if (current_key_compare > 0) {
        right = restart_index_;
      } else {
        // We're seeking to the key we're already at.
        return;
      }
    }
   /**
    * 使用二分查找法
    */
    while (left < right) {
      uint32_t mid = (left + right + 1) / 2;
      uint32_t region_offset = GetRestartPoint(mid);
      uint32_t shared, non_shared, value_length;
      const char* key_ptr =
          DecodeEntry(data_ + region_offset, data_ + restarts_, &shared,
                      &non_shared, &value_length);
      if (key_ptr == nullptr || (shared != 0)) {
        CorruptionError();
        return;
      }
      Slice mid_key(key_ptr, non_shared);
      if (Compare(mid_key, target) < 0) {
        // Key at "mid" is smaller than "target".  Therefore all
        // blocks before "mid" are uninteresting.
        left = mid;
      } else {
        // Key at "mid" is >= "target".  Therefore all blocks at or
        // after "mid" are uninteresting.
        right = mid - 1;
      }
    }

    assert(current_key_compare == 0 || Valid());
    /**
     * left == restart_index_ : 二分法找到的重启点区域就是迭代器所在的区域
     * current_key_compare < 0 : 当前键小于目标键
     */
    bool skip_seek = left == restart_index_ && current_key_compare < 0;
    if (!skip_seek) {
      SeekToRestartPoint(left); // 移动当前的block 到新的重启点
    }
    // Linear search (within restart block) for first key >= target
    while (true) {
      if (!ParseNextKey()) {// 解析下一个key
        return;
      }
      if (Compare(key_, target) >= 0) {
        return;
      }
    }
  }

猜你喜欢

C++多线程: blog.csdn.net/luog_aiyu/a… 一文了解LevelDB数据库读取流程:blog.csdn.net/luog_aiyu/a… 一文了解LevelDB数据库写入流程:blog.csdn.net/luog_aiyu/a… 关于LevelDB存储架构到底怎么设计的:blog.csdn.net/luog_aiyu/a…

PS

你的赞是我很大的鼓励 我是darkchink,一个计算机相关从业者,加我免费领取计算机相关书籍和资料 vx 二维码见: www.cnblogs.com/DarkChink/p…