Block的基本信息
Block 是组成SSTable文件中的基本单元,主要有以下类型
- 数据块(Data Block):存储实际的键值对数据,按键排序并使用前缀压缩减少空间占用。
- 过滤块(Filter Block):包含布隆过滤器,用于快速判断一个键是否可能存在,避免不必要的磁盘读取。
- 元数据块(Meta Block):存储关于SSTable文件的额外元数据信息,如统计数据或特定功能的配置。
- 元数据索引块(Metaindex Block):保存指向各个元数据块的索引,方便查找特定类型的元数据。
- 索引块(Index Block):存储数据块的索引信息,记录每个数据块的最大键和偏移量,用于定位特定键所在的数据块。
数据块(基本block)
数据块(Data Block)作为LevelDB块系统的基本单元
特性
- 前缀压缩: 优化存储空间
- 重启点: 优化访问效率
前缀压缩
前缀压缩是一种数据结构优化技术,通过存储共享相同前缀的字符串来减少存储空间。
例子
在示例中:
- 有几个单词:apple, applet, application, apples, apply
- 它们都共享前缀"a"和"pp"
- 通过前缀压缩,共同前缀只存储一次,然后为每个单词分别存储其余部分 优势:节省存储空间,可以加快查找速度。
重启点
重启点定义:完整存储键(不使用前缀压缩)的位置 默认设置:每16个键值对设置一个重启点 实现逻辑:当计数器达到重启间隔(16)时,记录当前位置为重启点,并重置计数器
例子
假设有100个键值对, restart_interval= 16
- 键1:完整存储 (重启点1)
- 键2-16:与前一个键使用前缀压缩
- 键17:完整存储 (重启点2)
- 键18-32:与前一个键使用前缀压缩 以此类推...
- 键97:完整存储 (重启点7)
- 键98-100:与前一个键使用前缀压缩
实现方式
具体实现方式有两个组件
- Block:提供数据访问的能力
- 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的生命周期管理能力
- 通过内置的 iterator 来提供对数据的遍历和检索能力
- 通过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);
...
}
简单来所,读取包含目标键的数据块并在其中搜索目标键
- 先通过BlockReader函数读取并解析一个数据块(Block对象),然后创建该块的迭代器,
- 使用Seek(k)在块中查找与目标键k匹配的条目。
Block读取的特征如下
- 启发式的局部性优化
- 二分法查找优化
逻辑细节
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…