本小节实现 Block
部分, Block
是 SSTable
的基本单位, 也是 LSM Tree
的最小读写单位。
本章完整代码参见:
https://github.com/ToniXWD/toni-lsm/tree/master/src/block
https://github.com/ToniXWD/toni-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
和value
key_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) ➜ toni-lsm git:(master) xmake run test_block |