美文网首页
LevelDB 布隆过滤器

LevelDB 布隆过滤器

作者: wayyyy | 来源:发表于2024-02-13 18:42 被阅读0次

    Bloom Filter 是一种空间效率很高的随机数据结构,由1970年布隆提出,它实际上是一个很长的二进制向量(位图)和 一系列随机映射函数(哈希函数)组成,布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
    错误识别率在于:如果布隆过滤器判定一个元素不存在,那么可以得出该元素肯定不存在,但如果判定一个元素存在,则有一定的误报率。一句话总结:布隆过滤器可以判定一个元素一定不存在或者可能存在

    布隆过滤器原理
    布隆过滤器.png

    如果我们要映射一个值到布隆过滤器中,需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位 置为1。


    baidu.png

    假设这里对 "baidu" 分别对应3次hash。并将对应的bit 位置为1,如上图所示。

    现在,这里我们再存一个 "tencent",图变为:

    tencent.png

    这里,第3位的 bit 重复了,所以还是如此。

    假设现在,我们查找 "taobao" 返回 bit 位是1 4 7,查看图表,我们可以肯定的说这个值是不存在的。

    但假设我们查找"ali",那返回的bit位都是0 3 8,那么我们能说"ali"一定存在吗?答案是不可以,只能说可能存在。因为随着增加的值越来越多,被置为1的bit位也会越来越多,这样的值即使没有被存储过,但万一hash函数返回的3个bit都被其他值置为了1,那么程序还是会判断这个值存在。

    这就是上面我们所说的误报率了,从上面也可以看出:怎么避免这种冲突就成了布隆过滤器是否误报的关键了。

    如何选择哈希函数的个数和布隆过滤器的长度。

    假设这里有 n 个整数的 set,以及一个 m 位的bit 数组,以及k个哈希函数。m[i] 表示第 i 个 bit 位。

    很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

    哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

    那么如何选择业务的 k 和 m 值:
    k = \frac{m}{n}ln2
    m = - \frac{nlnp}{(ln2)^{2}}
    k 为哈希函数个数, m 为布隆过滤器长度, n 为插入的元素个数, p 为误报率

    LevelDB中布隆过滤器的实现

    LevelDB中布隆过滤器代码实现在util/bloom.cc。
    LevelDB 首先定义了 FilterPolicy 接口,布隆过滤器是这一个接口的实现。

    class LEVELDB_EXPORT FilterPolicy 
    {
        public:
            virtual ~FilterPolicy();
            // 过滤器名称
            virtual const char* Name() const = 0;
            // 创建1个过滤器
            virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const = 0;
            virtual bool KeyMayMatch(const Slice& key, const Slice& filter) const = 0;
    };
    

    接下来,我们看怎么实现接口中的每个方法:

    class BloomFilterPolicy 
    {
        public:
            explicit BloomFilterPolicy(int bits_per_key) : bits_per_key_(bits_per_key)
            {
                k_ = static_cast<size_t>(bits_per_key * 0.69); // 0.69 约等于 ln(2)
                if (k_ < 1)
                    k_ = 1;
                if (k_ > 30)
                    k_ = 30;
            }
    
        private:
            size_t bits_per_key_;
            size_t k_;
    }
    

    bits_per_key,等同于\frac{m}{n}
    k_` 等同于是hash函数的个数

    构建布隆过滤器 CreateFilter
    // keys 代表布隆过滤器中要插入的袁术数组
    // n 表示keys数组的个数
    // dst 最后布隆过滤器的内容
    void CreateFilter(const Slice* keys, int n, std::string* dst) const override
    {
        // n 表示键的个数,bits_per_key_表示 每个key使用的位数,所以 bits 表示使用的总的位数
        size_t bits = n * bits_per_key_;
        if (bits < 64)  bits = 64;
    
        size_t bytes = (bits + 7) / 8;  // 向上取整,因为最后1个字节需要记录k_
        bits = bytes * 8;
        
        const size_t init_size = dst->size();
        dst->resize(init_size + bytes, 0);
        dst->push_back(static_cast<char>(k_));     // 记录一下hash函数的个数
        char *array = &(*dst)[init_size];
        
        for (int i = 0; i < n; i++)
        {
            uint32_t h = BloomHash(keys[i]);    // 计算键的hash值
            const uint32_t delta = (h >> 17) | (h << 15); // Rotate right 17 bits
            // 理论上应该需要对每一个键计算k_次hash值,这里通过将对键计算得到的hash值加delta来模拟
            for (size_t j = 0; j < k_; j++)
            {
                const uint32_t bitpos = h % bits;
                // 访问m[i] 把相应位设置为1
                array[bitpos / 8] |= (1 << (bitpos % 8));
                h += delta;
            }
        }
    }
    

    理论上应该需要对每一个键计算k_次hash值,这里通过将对键计算得到的hash值加delta来模拟,这里的做法可以参考论文:Less hashing, same performance: Building a better Bloom filter

    模拟使用方式如下:

    std::vector<Slice> tmp_keys;
    
    tmp_keys_.push_back("he");
    tmp_keys_.push_back("hello");
    
    BloomFilterPolicy bloom_filter_policy(8);
    std::string filter;
    bloom_filter_policy.CreateFilter(&tmp_keys[0], tmp_keys.size(), &filter);
    
    KeyMayMatch
    virtual bool KeyMayMatch(const Slice& key, const Slice& bloom_filter) const 
    {
        // bloom_filter是一个bit数组
        // 只不过是用Slice字符串的方式组装起来的
        const size_t len = bloom_filter.size();
        if (len < 2) return false;
        // 获得相应的内存区域
        const char* array = bloom_filter.data();
        // 总共的bits数目
        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.
        // 取得k哈希函数的数目
        const size_t k = array[len-1];
        // 对于大于30个哈希函数的情况,这里直接返回存在
        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中的应用

    参考资料
    1、https://zhuanlan.zhihu.com/p/43263751
    2、https://www.jasondavies.com/bloomfilter/
    3、https://zhuanlan.zhihu.com/p/45942533

    相关文章

      网友评论

          本文标题:LevelDB 布隆过滤器

          本文链接:https://www.haomeiwen.com/subject/hysjadtx.html