有关BloomtFilter的理论知识就不详细介绍了,有兴趣的可以点这里BloomFilter理论推导。
本篇主要是记录下BloomFilter一些关键知识点并梳理下leveldb中对BloomFilter的应用。
理论
影响BlootFilter准确率的参数有:
- 哈希函数的个数 k;
- 布隆过滤器位数组容量 m;
- 布隆过滤器插入的数据量 n
为保持准确率得出的结论如下:
- 当哈希函数个数 k=ln(2)*(m/n) 时查询出错的概率最小。
- 当不考虑最优哈希函数k时,假设错误率为 ϵ ,在查询错误率不大于 ϵ 的情况下
m >= n log2(1/є) 才能表示n个元素的集合。 - 当考虑哈希函数个数最优时同时错误率不能超过 ϵ ,则m >= 1.44*n log2(1/є) 。
举例:
布隆过滤器位数组是m,key数量是n,则每个key消耗的内存是bits_per_key = m / n,
假设查询错误率ϵ,不超过0.01,通过公式m >= 1.44*n log2(1/є) ,可得出 m/n ~= 9.567。也就是说
给每个key分配10个bit位(即bits_per_key = 10)可将错误率控制在0.01。等下leveldb内部实现即可看到其bits_per_key也是默认为10的。
优点
-
BloomFilter不需要存储元素的实际数据到容器中去一个个比较元素是否存在,只需要对应的位
来标识元素是否存在,就像上文所描述的,只需要10个bit就可以判断一个key是否存在,这在
海量数据查询判断时是非常节约空间的。 -
BloomFilter的时间效率都是常数级别的O(k),k是hash函数个数。由于k个hash函数式相
互独立,在进行k个hash计算时可以并行计算进一步加速插入、查找等。
缺点
虽然通过多个hash函数来降低hash collision,但仍有可能将不属于这个集合的元素误认为在这个集合中。在leveldb这种允许低错误率的应用场景中还是可以接受的,leveldb在Filter中查找到以后会再次去DataBlock中查找确认的。
参数设定
leveldb中针对BloomFilter的hash函数个数和bits_per_key值设定如下:
1.hash个数
explicit BloomFilterPolicy(int bits_per_key) : bits_per_key_(bits_per_key) {
// We intentionally round down to reduce probing cost a little bit
k_ = static_cast<size_t>(bits_per_key * 0.69); // 0.69 =~ ln(2)
if (k_ < 1) k_ = 1;
if (k_ > 30) k_ = 30;
}
2.bits_per_key
// Return a new filter policy that uses a bloom filter with approximately
// the specified number of bits per key. A good value for bits_per_key
// is 10, which yields a filter with ~ 1% false positive rate.
//
// Callers must delete the result after any database that is using the
// result has been closed.
//
// Note: if you are using a custom comparator that ignores some parts
// of the keys being compared, you must not use NewBloomFilterPolicy()
// and must provide your own FilterPolicy that also ignores the
// corresponding parts of the keys. For example, if the comparator
// ignores trailing spaces, it would be incorrect to use a
// FilterPolicy (like NewBloomFilterPolicy) that does not ignore
// trailing spaces in keys.
LEVELDB_EXPORT const FilterPolicy* NewBloomFilterPolicy(int bits_per_key);
从上面这段注释中我们可知,leveldb的建议设定是bits_per_key = 10,就可以将查询错误率控制在1%了。
当然,用户是可以指定过滤器的,也可以指定BloomFilter的bits_per_key。但是leveldb一般还是建议使用BloomFilter,且bits_per_key = 10就可以了。
源码解析
Bloom.cc
namespace leveldb {
namespace {
//计算哈希值的函数
static uint32_t BloomHash(const Slice& key) {
return Hash(key.data(), key.size(), 0xbc9f1d34);
}
class BloomFilterPolicy : public FilterPolicy {
public:
explicit BloomFilterPolicy(int bits_per_key) : bits_per_key_(bits_per_key) {
//计算哈希函数个数,控制在1~30个范围。
// We intentionally round down to reduce probing cost a little bit
k_ = static_cast<size_t>(bits_per_key * 0.69); // 0.69 =~ ln(2)
if (k_ < 1) k_ = 1;
if (k_ > 30) k_ = 30;
}
const char* Name() const override { return "leveldb.BuiltinBloomFilter2"; }
//创建一个BloomFilter数据。传入的值:
//1、keys: key列表;
//2、n: key的个数;
//3、dst: 用于存放BloomFilter的数据地址
void CreateFilter(const Slice* keys, int n, std::string* dst) const override {
//计算出中的需要的bits个数
// Compute bloom filter size (in both bits and bytes)
size_t bits = n * bits_per_key_;
//bits太小的话会导致很高的查询错误率,
//这里强制bits个数不能小于64
// For small n, we can see a very high false positive rate. Fix it
// by enforcing a minimum bloom filter length.
if (bits < 64) bits = 64;
//向上按8bit,一个Byte对齐
size_t bytes = (bits + 7) / 8;
bits = bytes * 8;
//扩展下要存储BloomFilter的内存空间,
//并在尾部一个Byte存哈希函数的个数。
const size_t init_size = dst->size();
dst->resize(init_size + bytes, 0);
dst->push_back(static_cast<char>(k_)); // Remember # of probes in filter
//接下来开始存储每个key值。
char* array = &(*dst)[init_size];
for (int i = 0; i < n; i++) {
//BloomFilter理论是通过多个hash计算来减少冲突,
//但leveldb实际上并未真正去计算多个hash,而是通过
//double-hashing的方式来达到同样的效果。
//double-hashing的理论如下:
// h(i,k) = (h1(k) + i*h2(k)) % T.size
// h1(k) = h, h2(k) = delta, h(i,k) = bitpos
//1、计算hash值;
//2、hash值的高15位,低17位对调
//3、按k_个数来存储当前hash值。
//3-1、计算存储位置;
//3-2、按bit存;
//3-3、累加hash值用于下次计算
// 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); // Rotate right 17 bits
for (size_t j = 0; j < k_; j++) {
const uint32_t bitpos = h % bits;
array[bitpos / 8] |= (1 << (bitpos % 8));
h += delta;
}
}
}
//值的匹配就是插入的逆过程了。
bool KeyMayMatch(const Slice& key, const Slice& bloom_filter) const override {
//1、插入时按1Byte对齐;
//2、尾部插入了一个Byte的hash个数
//所以大小不能下雨2个字节,这样理解不知对否
const size_t len = bloom_filter.size();
if (len < 2) return false;
//除去尾部的1Byte对应的hash个数,就是当前位数组容器的大小
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;
}
//1、计算查询key对应的hash值
//2、按插入规则去 &,只要有1bit不相同,那就不存在。
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;
h += delta;
}
return true;
}
private:
//每个key需要多少bit来存储表示
size_t bits_per_key_;
//hash函数的个数
size_t k_;
};
} // namespace
const FilterPolicy* NewBloomFilterPolicy(int bits_per_key) {
return new BloomFilterPolicy(bits_per_key);
}
} // namespace leveldb
调用流程
1.创建流程
2.查询流程
这里只记录了key从TableCache开始流程,上层过程未记录。
总结
对于节约空间的BloomFilter是非常使用leveldb的,leveldb针对key的很多设计都是尽量减少多余的空间占用。
巨人的肩膀:
https://blog.csdn.net/carbon06/article/details/80118954
https://juejin.im/post/5ecfcc12518825432f59f8ca
https://www.shuzhiduo.com/A/GBJrN9GZJ0/