Block的基本信息
Block 是组成SSTable文件中的基本单元,主要有以下类型
- 数据块(Data Block):存储实际的键值对数据,按键排序并使用前缀压缩减少空间占用。
- 过滤块(Filter Block):包含布隆过滤器,用于快速判断一个键是否可能存在,避免不必要的磁盘读取。
- 元数据块(Meta Block):存储关于SSTable文件的额外元数据信息,如统计数据或特定功能的配置。
- 元数据索引块(Metaindex Block):保存指向各个元数据块的索引,方便查找特定类型的元数据。
- 索引块(Index Block):存储数据块的索引信息,记录每个数据块的最大键和偏移量,用于定位特定键所在的数据块。
核心逻辑
目的:让Table在读取对应的data block,能低成本的skip掉一些数据块,从而降低整个系统磁盘IO的开销。类似将所有的key使用64位来做一个指纹出来,然后每次对数据块开始检索前,先匹配指纹,减少开销。
核心逻辑-布隆过滤器-CreateFilter&KeyMayMatch
以上的所有逻辑都是职能逻辑,接下来,我们来看常规情况下在 leveldb中内置的key和特征值之间的映射生成和读取的逻辑。
- CreateFilter 对应上面的GenerateFilter
- KeyMayMatch 对应上面的KeyMayMatch
CreateFilter

- 计算布隆过滤器大小:根据键的数量(n)和每个键的位数(bits_per_key_)计算总位数
- 确保最小大小:如果计算出的位数小于64,强制设为64位,确保最低限度的有效性
- 计算字节数和位数:将位数转换为字节数,使用(bits + 7) / 8的整数除法技巧,然后重新校准位数
- 准备存储空间:调整目标字符串大小并初始化为0,同时存储探测次数k_供后续查询使用
- 循环处理每个键:遍历所有输入键,为每个键计算哈希值并设置对应的位
- 计算哈希值:使用BloomHash函数为当前键生成哈希值
- 计算哈希增量:通过位运算(h >> 17) | (h << 15)生成增量值,用于后续的双重哈希
- 循环设置位:对每个键设置k个位,使用模运算h % bits确定位置,然后通过位运算设置相应的位,每次迭代后哈希值加上增量值
源代码
cpp
void CreateFilter(const Slice* keys, int n, std::string* dst) const override {
// Compute bloom filter size (in both bits and bytes)
size_t bits = n * bits_per_key_;
// 如果bits小于64,则强制设置为64, 至少保障64位来防止false positive
if (bits < 64) bits = 64;
// 这里有个trick, 为了避免浮点数除法, 使用(bits + 7) / 8 来计算bytes 这样只有整数除法
size_t bytes = (bits + 7) / 8;
bits = bytes * 8;
const size_t init_size = dst->size();
dst->resize(init_size + bytes, 0);
dst->push_back(static_cast<char>(k_)); // 存入探测数量 k_
// 行代码的目的就是获取新过滤器数据应该开始写入的位置:如果字符串为空,就是开头;如果已有数据,就是末尾。
// 这使得系统可以高效地在一个字符串中存储多个连续的过滤器数据,而不需要为每个过滤器创建单独的字符串
char* array = &(*dst)[init_size];
for (int i = 0; i < n; i++) {
// Use double-hashing to generate a sequence of hash values.
// See analysis in [Kirsch,Mitzenmacher 2006].
uint32_t h = BloomHash(keys[i]); // 核心方法
const uint32_t delta = (h >> 17) | (h << 15); // 这里又是一个新trick, 使用位移操作来代替乘法 这样能够使增量与原哈希值差异大,但保持了良好的随机性
for (size_t j = 0; j < k_; j++) {
const uint32_t bitpos = h % bits;//
array[bitpos / 8] |= (1 << (bitpos % 8));
h += delta;
}
}
}
KeyMayMatch
本质上就是上述方法CreateFilter的逆运算,这里就直接放代码了,就不赘述了
cpp
bool KeyMayMatch(const Slice& key, const Slice& bloom_filter) const override {
const size_t len = bloom_filter.size();
if (len < 2) return false;
const char* array = bloom_filter.data();
const size_t bits = (len - 1) * 8;
// Use the encoded k so that we can read filters generated by
// bloom filters created using different parameters.
const size_t k = array[len - 1];
if (k > 30) {
// Reserved for potentially new encodings for short bloom filters.
// Consider it a match.
return true;
}
uint32_t h = BloomHash(key);
const uint32_t delta = (h >> 17) | (h << 15); // Rotate right 17 bits
for (size_t j = 0; j < k; j++) {
const uint32_t bitpos = h % bits;
if ((array[bitpos / 8] & (1 << (bitpos % 8))) == 0) return false;// 如果位运算不一致就 直接返回false
h += delta;
}
return true;
}
举例说明
对外提供服务

- 开始查找键k
- 首先通过索引块找到可能包含目标键的数据块位置
- 过滤器是否存在 (filter != nullptr),如果数据库没有配置过滤策略,过滤器就不存在
- handle解析是否成功 (handle.DecodeFrom(...).ok()),确保从索引块中能正常解析出数据块的位置信息
- 键是否肯定不存在 (!filter->KeyMayMatch(...)),使用布隆过滤器检查键是否可能存在
- 如果三个条件都满足(过滤器存在、handle解析成功、键肯定不存在),则可以跳过数据块读取,直接返回Not Found, 否则,需要读取数据块并在数据块中搜索目标键
作用: 这种优化可以避免对不存在键的磁盘I/O操作,大幅提升查询性能 以下是源代码:
cpp
Status Table::InternalGet(const ReadOptions& options, const Slice& k, void* arg,
void (*handle_result)(void*, const Slice&,
const Slice&)) {
... // 逻辑省略
FilterBlockReader* filter = rep_->filter;
BlockHandle handle;
// 1. filter != nullptr: 如果 db没有设计过滤策略就是nullptr 即:
// 2. handle.DecodeFrom(&handle_value).ok(): 是否能够从索引块中正常解析出handle
// 3. !filter->KeyMayMatch(handle.offset(), k))检查当前的数据块是否有可能存在对应的key
// 组合起来就是为了判断当前的value 肯定不存在对应的数据块中
if (filter != nullptr && handle.DecodeFrom(&handle_value).ok() &&
!filter->KeyMayMatch(handle.offset(), k)) {
// 通过 KeyMayMatch 方法提前判断不存在,直接跳过
} else
{
....// 遍历对应的数据块
}
....// 逻辑省略
}
实现细节
基本数据结构
- FilterBlockBuilder:负责在写入阶段收集键并构建过滤器块
2. FilterBlockReader:负责在查询阶段解析过滤器块并快速判断键是否可能存在
调用入口GenerateFilter&&KeyMayMatch
- GenerateFilter: 根据当前的kv键来生成对应的方法
- KeyMayMatch:判断当前的块是否有可能存在对应的key
GenerateFilter
cpp
void FilterBlockBuilder::GenerateFilter() {
...
// ***************************************************************
policy_->CreateFilter(&tmp_keys_[0], static_cast<int>(num_keys), &result_);
// 这里是一个虚函数调用,如果正式工作就是具体的逻辑来进行调用,使用了策略模式进行实现
// ***************************************************************
...
}
这里的核心方法是 CreateFilter 传入 将key打平的中间变量 tmp_keys ,然后将result_返回出来,result_保存将当前的所有key都使用hash压缩后的特征值
KeyMayMatch

- 计算过滤器索引:将数据块偏移量右移 base_lg_ 位(默认11位)得到对应的过滤器索引
- 验证索引有效性:检查索引是否在有效范围内(小于过滤器数量)
- 获取过滤器位置:从偏移量数组中解码出过滤器的起始位置和结束位置
- 检查范围有效性:确保过滤器的位置信息合理
- 使用布隆过滤器:对于有效的过滤器,调用布隆过滤器检查键是否可能存在
源代码
cpp
bool FilterBlockReader::KeyMayMatch(uint64_t block_offset, const Slice& key) {
uint64_t index = block_offset >> base_lg_;// 默认是 11位
if (index < num_) {
uint32_t start = DecodeFixed32(offset_ + index * 4);// 从当前的offset中解析出两个位置 一个是开头,另一个是结束
uint32_t limit = DecodeFixed32(offset_ + index * 4 + 4);
if (start <= limit && limit <= static_cast<size_t>(offset_ - data_)) {
Slice filter = Slice(data_ + start, limit - start);
//*******************************************************
return policy_->KeyMayMatch(key, filter);// 使用 布隆过滤器来进行匹配
//*******************************************************
} else if (start == limit) {
// 如果是空就没必要进行检索了
return false;
}
}
return true; // 如果报错也是默认 是true,扫描当前的 data
}
policy_->KeyMayMatch(key, filter) 这个方法是核心方法,传入要匹配的key,和过滤区的内存指针,从而能够根据 filter 对key进行快速匹配
猜你喜欢
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...