Lab 3.1 Block 实现
1 准备工作
老套路, 我们先理一下Block
的数据结构, 看看头文件定义:
// include/block/block.h
class Block : public std::enable_shared_from_this<Block> {
friend BlockIterator;
private:
std::vector<uint8_t> data; // 对应架构图的 Data
std::vector<uint16_t> offsets; // 对应架构图的 Offset
size_t capacity;
struct Entry {
std::string key;
std::string value;
uint64_t tranc_id;
};
// ...
};
这里可能涉及C++的新特性:
public std::enable_shared_from_this<Block>
std::enable_shared_from_this
是 C++11 引入的一个标准库特性,它允许一个对象安全地创建指向自身的std::shared_ptr
。这里先简单说明一下, 后续实现迭代器的时候就知道其作用了。
这里主要对data
和offsets
这两个数据结构进行说明, 他们在构建阶段和读取阶段存在一定区别, 首先还是给出架构图:
构建阶段
当我们从Skiplist
拿到数据构建一个SST
时, SST
需要逐个构建Block
, 这个Block
在构建时步骤如下:
- 逐个将编码的键值对(也就是
Entry
)写入data
数组, 同时将每个Entry
的偏移量记录在内存中的offsets
数组中。 - 当这个
Block
容量达到阈值时,Block
构建完成, 你需要将offsets
数组写入到Block
的末尾。 - 还需要再
Block
末尾写入一个Entry Num
值, 用于标识这个Block
中键值对的数量, 从而在解码时获取Offset
的其实位置(因为每个Entry Offset
大小是固定的整型值) - 当前
Block
构建完成,SST
开始构建下一个Block
。
这里之所以将先将键值对持久化到
data
数组, 而元信息暂存于内存的offsets
数组, 是因为Data
是在数据部分之后的的Offset
部分的偏移需要再键值对完全写入Data
部分后才能确定
解码阶段
解码阶段, 直接将Data
、Offset
解码形成内存中的Block
的实例以为上层组件提供查询功能,同时如果实现了缓存池,需要再缓存池中进行记录。
2 代码实现
你需要修改的函数都在src/block/block.cpp
中。
2.1 Block 编码和解码
这里你先不要管这个Block
是哪里来的, 就当它已经存在, 实现编码和解码的功能:
std::vector<uint8_t> Block::encode() {
// TODO Lab 3.1 编码单个类实例形成一段字节数组
return {};
}
std::shared_ptr<Block> Block::decode(const std::vector<uint8_t> &encoded,
bool with_hash) {
// TODO Lab 3.1 解码字节数组形成类实例
return nullptr;
}
这里特别说明, encode
时的数据是不包括校验的哈希值的 因为哈希值是在SST
控制Block
构建过程中计算的, 但在decode
时可以通过with_hash
参数来指示传入的encoded
是否包含哈希值, 如果包含哈希值, 则需要先校验哈希值是否正确, 校验失败则抛出异常。
之所以
encode
不计算哈希值,decode
按需计算哈希值, 其实是作者初版代码设计不佳, 这里先不纠结了, 后续可能会进行优化, 如果你有优化方案, 可对代码进行修改后提PR
编解码时你需要注意数据的格式, 如果校验格式错误, 你需要抛出异常, 否则错误将非常难以
Debug
2.2 局部数据编解码函数
对于二进制数据, 你需要按照设计的编码结构获取其key
, value
和tranc_id
, 这里我们实现几个辅助函数:
// 从指定偏移量获取entry的key
std::string Block::get_key_at(size_t offset) const {
// TODO Lab 3.1 从指定偏移量获取entry的key
return "";
}
// 从指定偏移量获取entry的value
std::string Block::get_value_at(size_t offset) const {
// TODO Lab 3.1 从指定偏移量获取entry的value
return "";
}
uint16_t Block::get_tranc_id_at(size_t offset) const {
// TODO Lab 3.1 从指定偏移量获取entry的tranc_id
// ? 你不需要理解tranc_id的具体含义, 直接返回即可
return 0;
}
2.3 构建 Block
Block
构建是由SST
控制的, 其会不断地调用下面这个函数添加键值对:
bool Block::add_entry(const std::string &key, const std::string &value,
uint64_t tranc_id, bool force_write) {
// TODO Lab 3.1 添加一个键值对到block中
// ? 返回值说明:
// ? true: 成功添加
// ? false: block已满, 拒绝此次添加
return false;
}
这里需要注意, force_write
参数表示是否强制写入, 如果为true
, 则不管Block
是否已满, 都强制写入, 否则如果Block
已满, 则拒绝此次写入。
Block
是否已满的判断将当前数据容量与成员变量capacity
进行比较, capacity
在Block
初始化时由SST
传入, 表示一个Block
的最大容量。
如果你需要一些使用
config.toml
中预定义的一些阈值变量或者其他常来那个, 你也可以通过TomlConfig::getInstance().getXXX
的方式获取
2.4 二分查询
Block
构建时是通过SST
遍历Skiplist
的迭代器调用add_entry
实现的, 因此Block
的数据是有序的, 你需要实现一个二分查找函数, 用于在Block
中查找指定key
所属的Entry
在offset
元数据中的索引:
std::optional<size_t> Block::get_idx_binary(const std::string &key,
uint64_t tranc_id) {
// TODO Lab 3.1 使用二分查找获取key对应的索引
return std::nullopt;
}
get_value_binary
函数中会调用get_idx_binary
函数, 并返回指定key
的value
:
// 使用二分查找获取value
// 要求在插入数据时有序插入
std::optional<std::string> Block::get_value_binary(const std::string &key,
uint64_t tranc_id) {
auto idx = get_idx_binary(key, tranc_id);
if (!idx.has_value()) {
return std::nullopt;
}
return get_value_at(offsets[*idx]);
}
3 测试
如果成功完成了上述的所有函数, 你应该如下运行测试并得到结果:
✗ xmake
[100%]: build ok, spent 1.94s
✗ xmake run test_block
[==========] Running 10 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 10 tests from BlockTest
[ RUN ] BlockTest.DecodeTest
[ OK ] BlockTest.DecodeTest (0 ms)
[ RUN ] BlockTest.EncodeTest
[ OK ] BlockTest.EncodeTest (0 ms)
[ RUN ] BlockTest.BinarySearchTest
[ OK ] BlockTest.BinarySearchTest (0 ms)
[ RUN ] BlockTest.EdgeCasesTest
[ OK ] BlockTest.EdgeCasesTest (0 ms)
[ RUN ] BlockTest.LargeDataTest
[ OK ] BlockTest.LargeDataTest (0 ms)
[ RUN ] BlockTest.ErrorHandlingTest
[ OK ] BlockTest.ErrorHandlingTest (1 ms)
[ RUN ] BlockTest.IteratorTest
test/test_block.cpp:225: Failure
Expected equality of these values:
count
Which is: 0
test_data.size()
Which is: 100
你应该能通过BlockTest.IteratorTest
之前的所有单元测试, BlockTest.IteratorTest
这个测试会测试Block
的迭代器功能, 因为Block
的迭代器功能还没有实现, 所以会失败, 这是符合预期的。
4 下一步
现在进入下一步前, 你可以先思考:
- 如何实现
Block
的迭代器 - 为什么我们需要让
Block
类继承std::enable_shared_from_this<Block>
?
带着这些疑问, 欢迎开启下一章