bloomfilter
bloomfilter在Nosql、大数据的去重、判断数据是否存在等领域有着广泛的应用。 它是一种空间效率极高的概率型算法和数据结构,用于判断一个元素是否在集合中(类似Hashset), 其核心一个很长的二进制向量和一系列hash函数。
常见应用场景:
- Google著名的分布式数据库Bigtable以及Hbase使用了布隆过滤器来查找不存在的行或列,以及减少磁盘查找的IO次数;
- 文档存储检查系统也采用布隆过滤器来检测先前存储的数据;
- Goole Chrome浏览器使用了布隆过滤器加速安全浏览服务;
- 垃圾邮件地址过滤;
- 爬虫url地址去重;
- 解决缓存穿透问题
BloomFilter的原理:
要实现去重或者判断数据是否存在, 常规的方法有一下几个:
- Hash表:
- 集合:
- 位图
假设有10240个int整数,进行去重;
方法 | 空间 | 时间 | 前提条件 |
---|---|---|---|
hash | 200KB | O(1) | 数据量不能超过内存 |
集合 | 10KB | O(n) | 数据量不能超过内存 |
位图 | 1280B | O(1) | 指定范围内的有限数据 |
有上面的表格可以看到, 位图在空间时间方面的效率最高, 但是它有一个严重的限制: 数据只能在有限的集合范围内, 这对于大数据来说, 尤其不能接受。为了充分利用位图的有点, 克服其限制, BloomFilter就正好可以满足这两个条件。
BloomFilter原理:
当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
如上图所示, 我们定义了一个16位的二进制向量, 3个hash 函数,这个3个函数hash的结果为0或者1, 该结果存放的位置为0 ~ 15之间, 将hash的结果的位置映射到二进制向量的index, 并保存结果.
对于输入数据input1, 得到的的结果存在于0, 8, 14,结果全部为1, 那么 说明input1 可能存在于指定的集合。
对于input2, 得到的结果存在于0,4,10, 有一个是0 ,那么说明input2一定不存在于指定的集合。
leveldb 中BloomFilter的使用
LevelDB采用的是分层的存储结构,那么当Get一个key的时候最坏情况就是在所有的层级上都查询一遍这个key,这个开销是非常大的,引入BloomFilter之后,利用BloomFilter能够快速判断“是否存在”的特点可以很快速的知道需不需要在这个Level中进行查询。
为了支持Filter机制,LevelDB为SSTable增加了一个新的MetaBlock类型:”filter” MetaBlock。如果数据库在打开时,指定了FilterPolicy,metaindex block内将会包含一条记录用来将”filter.“映射到针对该filter meta block的block handle。此处””代表了由filter policy的Name,也就是说在metaindex block内将会有一个KeyValue对用来寻址filter summary数据组成的meta block,对于该KeyValue对来说,Key就是”filter.“,value就是filter meta block的handle}。
Filter Block存储了一系列的filters。对于这些filters来说,filter i包含了以offset落在[ ibase … (i+1)base-1 ]的那个block的所有key为输入的FilterPolicy::CreateFilter()函数的输出结果,因此实际中一个filter可能对应不止一个block,但是肯定是整数个block,一个block不会跨越两个filter,这样可以快速地计算出一个block对应的filter,因为data index block中存储了data block的offset,直接根据offset和base取值就可以计算出该data block对应了第几个filter。只要知道了是第几个filter,根据Filter Block自身的结构,就可以直接访问该filter数据了。
namespace {
static uint32_t BloomHash(const Slice& key) {
return Hash(key.data(), key.size(), 0xbc9f1d34);
}
class BloomFilterPolicy : public FilterPolicy {
private:
size_t bits_per_key_;
size_t k_;
public:
explicit BloomFilterPolicy(int bits_per_key)
: bits_per_key_(bits_per_key) {
// We intentionally round down to reduce probing cost a little bit
//计算出hash 函数的个数k_, 并且限定个数在 1 =< k_ =< 30
k_ = static_cast<size_t>(bits_per_key * 0.69); // 0.69 =~ ln(2)
if (k_ < 1) k_ = 1;
if (k_ > 30) k_ = 30;
}
virtual const char* Name() const {
return "leveldb.BuiltinBloomFilter2";
}
virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const {
// Compute bloom filter size (in both bits and bytes)
// 计算n个key所需要的空间, 注意不要修改dst前面已经有的内容, 只是添加
size_t bits = n * bits_per_key_;
// 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;
size_t bytes = (bits + 7) / 8;
bits = bytes * 8;
// 将dst的内存扩大bytes 字节
const size_t init_size = dst->size();
dst->resize(init_size + bytes, 0);
// 第一个字节是hash 函数的个数, 后面是n个key的k_个函数计算后的值, 按字节存储
dst->push_back(static_cast<char>(k_)); // Remember # of probes in filter
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); // Rotate right 17 bits
for (size_t j = 0; j < k_; j++) {
//bitpos计算出该hash 函数结果所在的bit的index;
// bitpos/8代表字节的index, 1 << (bitpos % 8)代表目标字节的bit的index
const uint32_t bitpos = h % bits;
array[bitpos/8] |= (1 << (bitpos % 8));
h += delta;
}
}
}
virtual bool KeyMayMatch(const Slice& key, const Slice& bloom_filter) const {
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;
h += delta;
}
return true;
}
};
}
在LevelDB里面用FilterBlockbuilder类来生成SSTable的bloom filter block:
- keys_用来存储需要添加进bloom filter的key,每个key都简单的连接在keys_的后面;
- start_存储每个key在keys_中的偏移量;
- result_为当前已经计算得到的bloom filter的值;
- tmp_keys_用于在调用CreateFilter函数时传递参数;
- filter_offsets_用于存储构建的每一个bloom filter在当前filter block中的偏移量。
class FilterBlockBuilder {
public:
explicit FilterBlockBuilder(const FilterPolicy*);
// StartBlock函数在开始构建bloom filter前调用。因为bloom filter的构建策略是每2k个字节就构建一个bloom filter,
// 所以StartBlock函数会首先判断当前sstable中尚未构建bloom filter的数据是否已经达到了2k,
// 如果达到,就调用GenerateFilter()函数为sstable中尚未构建bloom filter的数据构建新的bloom filter。
// 如下3个函数的调用次序: StartBlock --> AddKey, AddKey ..., AddKey -->Finish
void StartBlock(uint64_t block_offset);
void AddKey(const Slice& key);
Slice Finish();
private:
void GenerateFilter();
const FilterPolicy* policy_;
std::string keys_; // Flattened key contents
std::vector<size_t> start_; // Starting index in keys_ of each key
std::string result_; // Filter data computed so far
std::vector<Slice> tmp_keys_; // policy_->CreateFilter() argument
std::vector<uint32_t> filter_offsets_;
// No copying allowed
FilterBlockBuilder(const FilterBlockBuilder&);
void operator=(const FilterBlockBuilder&);
};
通过FilterBlockReader类来读取filter block,
相关阅读
缓存中无值(未宕机) 互斥锁 缓存永不过期 缓存宕机 白名单 布隆过滤器 代码实现 前面的文章介绍了缓存的分类和使用的场景