大白话解析LevelDB: BlockBuilder

BlockBuilder

BlockBuilder 是一个 Block 的构建器,它会将 Block 的 Key-Value 以及重启点信息编码到一个char* buffer里。

什么是重启点,我们后面会讲到。

BlockBuilder 的用法如下:

通过Add(const Slice& key, const Slice& value)方法,我们可以将需要 Key-Value 添加到 BlockBuilder 的缓冲区里,最后再通过Finish()方法,获取到 Block 的内容。

class BlockBuilder {
   public:
    explicit BlockBuilder(const Options* options);

    BlockBuilder(const BlockBuilder&) = delete;
    BlockBuilder& operator=(const BlockBuilder&) = delete;

    // 恢复到一个空白 Block 的状态
    void Reset();

    // 往 block buffer 里添加一对 Key-Value
    void Add(const Slice& key, const Slice& value);

    // 将重启点信息也压入到 Block 缓冲区里,结束该 Block 的构建,
    // 然后返回 buffer_。
    Slice Finish();

    // 返回 Block 的预估大小。
    // 准确的说,是 Block 的原始大小,但是 Block 写入
    // SST 前会进行压缩,所以此处只能返回一个预估大小。
    size_t CurrentSizeEstimate() const;

    // 判断 buffer_ 是否为空
    bool empty() const { return buffer_.empty(); }
};

BlockBuilder 的代码实现

BlockBuilder::Add(const Slice& key, const Slice& value)

通过BlockBuilder::Add(const Slice& key, const Slice& value)方法,我们可以将 Key-Value 添加到 BlockBuilder 的缓冲区里,并且会对 Key 进行前缀压缩,每隔 N 个 Key 会添加一个重启点。

前缀压缩就是将 Key 的公共前缀去掉,只存储后面不同的部分。比如,Block 里最后一个 Key 是 “hello”,此时再插入一个 Key “hello_world”,那么该 Key 存储在 Block 里的内容就是 “_world”。

重启点的意思是,从该重启点开始,第一个 Key 不再进行前缀压缩,而是直接存储整个 Key。

举个例子,假设重启间隔 N = 3,那么 Block 里的 Key-Value 可能是这样的:

+-----+ +-----+ +-----+ +-----+ +-----+ +-----+
| kv1 | | kv2 | | kv3 | | kv4 | | kv5 | | kv6 |
+-----+ +-----+ +-----+ +-----+ +-----+ +-----+
                       ^
                       |
                       |
                       +
                 restart point

其中,k1 和 k4 是完整存储的,k2、k3、k5、k6 只存储前缀压缩后的结果。

那为什么需要重启点呢?Block 里所有 Key 都进行前缀压缩不行吗?

如果没有重启点的话,Block 里只有第一个 Key 是完整存储的,后面的 Key 存储的只是前缀压缩后的结果。这样当我们进行范围查找的时候,Block 里有 100 个 Key,而我们只需要最末尾的 10 个 Key,但也需要将前面所有的 Key 都解压出来,才能拿到最后的 10 个 Key的内容。 如果有了重启点,并且刚好重启间隔是 10,那么我们只需要解压最后一个重启点之后的 10 个 Key,就能拿到最后的 10 个 Key 的内容了。

OK,现在我们了解前缀压缩与重启点后,就阔以往下看BlockBuilder::Add的实现了:

  • 首先计算当前 Key 和上一个 Key 的共享前缀长度,记录到变量shared里。
  • 若满足重建重启点的条件,则将shared置为 0,表示当前 Key 不需要压缩存储。并且将重启点的位置记录到restarts_数组里。
  • shared(共享前缀长度)non_shared(非共享长度)value_size(value的长度)按顺序写入buffer_里。
  • Key 的非共享部分写入buffer_里。如果shared为 0,则非共享部分就是整个Key
  • Value写入buffer_里。
void BlockBuilder::Add(const Slice& key, const Slice& value) {
    // 获取上一个添加的 Key: last_key_ 
    Slice last_key_piece(last_key_);
    assert(!finished_);
    assert(counter_ <= options_->block_restart_interval);
    // 如果 buffer_ 非空,检查 key 是否大于最后一个被添加到 Block 中的 Key
    assert(buffer_.empty()  // No values yet?
           || options_->comparator->Compare(key, last_key_piece) > 0);

    // 初始化一个变量shared,
    // 用于记录新添加的键和上一次添加的键的共享前缀的长度。
    size_t shared = 0;

    // 如果 counter_ < block_restart_interval的话,
    // 说明还没有到重启点,计算当前键和上一次添加的键的共享前缀的长度。
    if (counter_ < options_->block_restart_interval) {
        // 计算当前键和上一次添加的键的共享前缀的长度 shared
        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 {
        // counter_ == block_restart_interval,
        // 满足新建重启点的条件了,重置 counter_ 为 0,
        // 并且将当前的偏移量压入 restarts_ 数组中,记录当前重启点的位置。
        // 重启点的第一个 Key 不需要进行前缀压缩,
        // 所以此时保持 shared 的值为 0 即可。
        restarts_.push_back(buffer_.size());
        counter_ = 0;
    }

    // 计算 key 和 last_key_ 的非共享长度
    const size_t non_shared = key.size() - shared;

    // 使用变长编码,将 "<shared><non_shared><value_size>" 写入 buffer_
    PutVarint32(&buffer_, shared);
    PutVarint32(&buffer_, non_shared);
    PutVarint32(&buffer_, value.size());

    // 将 Key 非共享内容压入 buffer_ 
    buffer_.append(key.data() + shared, non_shared);
    // 将完整的 Value 压入 buffer_
    buffer_.append(value.data(), value.size());

    // 更新 last_key_
    last_key_.resize(shared);
    last_key_.append(key.data() + shared, non_shared);
    assert(Slice(last_key_) == key);
    counter_++;
}

BlockBuilder::Finish()

BlockBuilder::Finish之前,已经通过BlockBuilder::Add将 Key-Value 添加到了buffer_里。

BlockBuilder::Finish会在buffer_的末尾添加重启点信息,并且将buffer_返回。

Slice BlockBuilder::Finish() {
    // 先把 restarts_ 中的所有重启点位置压入 buffer_ 中
    for (size_t i = 0; i < restarts_.size(); i++) {
        PutFixed32(&buffer_, restarts_[i]);
    }

    // 再将重启点的数量压入到 buffer_ 中
    PutFixed32(&buffer_, restarts_.size());

    // 设置结束标志位
    finished_ = true;
    
    // 返回完整的 buffer_ 内容
    return Slice(buffer_);
}

Block 的内容格式

现在我们可以结合BlockBuilder::FinishBlockBuilder::Add的实现,来看看最终的 Block 的内容格式是什么样的。

+----------------+
|    Key1:Value1 |
+----------------+
|    Key2:Value2 |
+----------------+
|       ...      |
+----------------+
|    KeyN:ValueN |
+----------------+
| Restart Point1 |
+----------------+
| Restart Point2 |
+----------------+
|       ...      |
+----------------+
| Restart PointM |
+----------------+
| Num of Restarts|
+----------------+

其中,每条Key:Value的格式如下:

+------------+----------------+-----------+---------------------+------------+
| shared_len | not_shared_len | value_len | not_shared_key_data | value_data |
+------------+----------------+-----------+---------------------+------------+
  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值