BloomFilter(布隆滤波器)原理与使用

BloomFilter在NoSql、大数据的去重、判断数据是否存在等领域有着广泛的应用。 它是一种空间效率极高的概率型算法和数据结构,用于判断一个元素是否在集合中(类似Hashset), 其核心一个很长的二进制向量和一系列hash函数。
常见应用场景:

  • Google著名的分布式数据库Bigtable以及Hbase使用了布隆过滤器来查找不存在的行或列,以及减少磁盘查找的IO次数;
  • 文档存储检查系统也采用布隆过滤器来检测先前存储的数据;
  • Goole Chrome浏览器使用了布隆过滤器加速安全浏览服务;
  • 垃圾邮件地址过滤;
  • 爬虫URL地址去重;
  • 解决缓存穿透问题

BloomFilter的原理:

要实现去重或者判断数据是否存在, 常规的方法有一下几个:

  1. Hash表:
  2. 集合:
  3. 位图
    假设有10240个int整数,进行去重;
方法 空间 时间 前提条件
hash 200KB O(1) 数据量不能超过内存
集合 10KB O(n) 数据量不能超过内存
位图 1280B O(1) 指定范围内的有限数据

有上面的表格可以看到, 位图在空间时间方面的效率最高, 但是它有一个严重的限制: 数据只能在有限的集合范围内, 这对于大数据来说, 尤其不能接受。为了充分利用位图的有点, 克服其限制, BloomFilter就正好可以满足这两个条件。
BloomFilter原理:
当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。

BloomFilter(布隆滤波器)原理与使用_第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(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(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 start_;     // Starting index in keys_ of each key
  std::string result_;            // Filter data computed so far
  std::vector tmp_keys_;   // policy_->CreateFilter() argument
  std::vector filter_offsets_;

  // No copying allowed
  FilterBlockBuilder(const FilterBlockBuilder&);
  void operator=(const FilterBlockBuilder&);
};

通过FilterBlockReader类来读取filter block,

你可能感兴趣的:(LevelDB)