本小节实现 Block 部分, Block 是 SSTable 的基本单位, 也是 LSM Tree 的最小读写单位。
本章完整代码参见:
https://github.com/Vanilla-Beauty/tiny-lsm/tree/master/src/block
https://github.com/Vanilla-Beauty/tiny-lsm/tree/master/include/block
代码不断迭代, 可能会和文章中的代码有出入, 但基本原理差不多
1 Block基础概念
1.1 Block与SST

介绍 Block 之前, 需要先说明什么是SST, SST 是 Sorted String Table 的缩写, 也就是有序字符串表, 是 LSM Tree 的基本存储单位, 也是 Block 的集合。当 MemTable 达到一定大小后, 就会被 flush 到 SST 中, SST 会被写入到磁盘中, 以此来保证数据的持久化。因此SST和Immutable MemTable是一一对应的关系。
而SST被划分为多个 Block, Block 是 SST 的最小读写单位, 也是 LSM Tree 的最小读写单位。这样划分的好处是可以减少读取的数据量, 提高读取性能。因为SSt是磁盘上的数据, 因此将其划分为多个 Block 可以减少读取的数据量, 提高读取性能。
1.2 Block的编码
Block 的编码方式有很多种, 这里我就参考了Mini-LSM采用最简单的编码方式, 也没有什么压缩算法, 其编码格式如下:
1 | ------------------------------------------------------------------------------------------------------------------ |
上图的
B表示一个字节
一个Block包含:Data Section、Offset Section和Extra Information三部分:
Data Section: 存放所有的数据, 也就是key和value的序列化数据- 每一对
key和value都是一个Entry, 编码信息包括key_len、key、value_len和valuekey_len和value_len都是2B的uint16_t类型, 用于表示key和value的长度key和value就是对应长度的字节数, 因此需要用varlen来表示。
- 每一对
Offset Section: 存放所有Entry的偏移量, 用于快速定位Extra Information: 存放num_of_elements, 也就是Entry的数量
这样编码满足了最基本的要求, 从最后的2个字节可以知道Block包含了多少kv对, 再从Offset Section中查询对应的kv对数据的偏移
2 Block代码实现
2.1 头文件定义
首先定义一个Block对应的类, 按照编码信息的需求, 可以如下定义:
1 | class BlockIterator; // 迭代器, 后续会介绍 |
这里可能涉及C++的新特性:
1 | public std::enable_shared_from_this<Block> |
std::enable_shared_from_this 是 C++11 引入的一个标准库特性,它允许一个对象安全地创建指向自身的std::shared_ptr。这里先简单说明一下, 后续实现迭代器的时候就知道其作用了。
2.2 编解码
先看编码:
1 | std::vector<uint8_t> Block::encode() { |
很简单, 基本上就是履行了之前编码格式的过程
2.1.3 解码
解码就是编码的逆过程, 需要注意的是, 后续的SST在每个block后可能会添加一个哈希校验值, 因此这里可以通过with_hash设置是否二进制序列包括哈希值的部分。
1 | std::shared_ptr<Block> Block::decode(const std::vector<uint8_t> &encoded, |
2.3 Block构建代码
Block的构建过程是这样的:
- 先在内存中插入数据, 即类定义中的
offsets,data等 - 当类定义的
data数据超过capacity时, 调用encode进行编码
这里就先给出最基础的构建Block中向内存插入kv数据的代码:
1 | bool Block::add_entry(const std::string &key, const std::string &value) { |
2.4 二分查找
由于sst和blcok的构建本质上就是按照我们之前实现的MemTable的SkipList迭代器插入数据, 因此其已经是排好序的, 查询时可以使用二分查找:
1 | std::optional<size_t> Block::get_idx_binary(const std::string &key) { |
这里不给出全部的代码了, 有兴趣可以看Github仓库
3 Block迭代器
3.1 处理this指针
现在实现我们的Block的迭代器。通过Block的编码格式可知, 只需要知道当前的索引, 就可以在Block中查询该索引的kv对, 因此迭代器只需要记录当前索引和原始Block的引用就可以了。
这也就是之前提到的std::enable_shared_from_this的作用,让我们的BlockIterator可以使用Block的智能指针, 为什么这样设计呢? 看下面的案例代码就知道了:
1 | TEST_F(BlockTest, IteratorTest) { |
迭代器对象BlockIterator是通过block->begin()获取的, 因此相当于类的成员函数返回的对象中需要当前对象的this指针, 因此C++11引入了std::enable_shared_from_this来使用shared_ptr来封装this指针, 但当前类必须也是由shared_ptr管理的, 正如代码中的:
1 | std::shared_ptr<Block> block = std::make_shared<Block>(4096); |
3.2 实现
这个实现其实很简单, 因为这里的逻辑是, sst负责从文件中读取block, 这里的block只需要读取后的内存中的数据结构, 所以也就是维护当前索引就是了, 另外这里有个小优化, 尽管有索引就在block中进行二分查找, 但这里还是用一个值进行缓存, 因为迭代器可能被反复读取值。
1 | class Block; |
这里的
std::optional也是一个智能指针, 其可以为空, 可以看做青春版的Rust的Option
代码实现只要是自增运算符和解引用运算符:
自增运算符
1 | BlockIterator &BlockIterator::operator++() { |
解引用运算符
1 | BlockIterator::value_type BlockIterator::operator*() const { |
4 单元测试
同样进行完整的单元测试:
1 |
|
结果:
1 | (base) ➜ Tiny-LSM git:(master) xmake run test_block |