布隆过滤器(Bloom Filter)原理和实现

当判断一个元素是否在一个集合的时候,比较常用的就是哈希表了,这种查询速度很快,并且查询准确,但是当数据非常大甚至到上亿级的时候,这时候内存和查询时间压力就会上来了,那么就可以牺牲一定的准确性而使用一种数据结构--布隆过滤器

布隆过滤器(Bloom Filter)是1970年由伯顿·霍华德·布隆(Burton Howard Bloom)提出的,它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

一句话说明布隆过滤器:
如果要查找一个元素一定在集合中,那么判断准确率不是百分之百的
但是如要果查找一个元素一定不在一个集合中,那么判断的准确率是百分之百的

应用场景:

应用场景非常多,凡是过滤大规模查询都可以考虑应用

先举例一个场景,如何在网站或者游戏注册的时候快速判断用户名没有注册过

这时候用户名全部在硬盘上存储,不可能挨个去扫描硬盘吧,那开销太大了,但是把全部用户名放到缓存里面,如果用户量过大,缓存全部放用户名也不现实,那么就可以将所有已注册用户名添加到布隆过滤器,当新用户注册时,如果返回"不存在",则肯定可用,如果返回"可能存在",再查询开销更大的数据库确认

在Redis中使用布隆过滤器,防止恶意查询不存在的 key 导致大量请求穿透到数据库(缓存穿透问题),查询缓存前,先检查布隆过滤器,若返回"不存在",则直接拒绝请求。但是由于布隆过滤器误判的特性,可能会有一些"恶意查询"能通过查询,但是已经过滤了大量的查询,放过一部分"恶意查询"完全可以接受,已经大大降低了系统负担

同样在数据库中也可以快速判断某个 RowKey 是否存在于某个 SSTable(存储文件)中,避免无效的磁盘扫描

原理:

假设我有三个散列函数

y = HashFun1(X)

y = HashFun2(X)

y = HashFun3(X)

有一个长度为15的Bit数组

①这时候要插入一个元素A,这个元素计算出三次散列函数

HashFun1(A) = 位置1

HashFun2(A) = 位置2

HashFun3(A) = 位置15

那么将这三个对应位置标记为1

②这时候再插入元素B

HashFun1(B) = 位置1

HashFun2(B) = 位置3

3号位置没有被占据,说明这个元素没有插入过

HashFun3(B) = 位置14

那么将这位置3和14对应位置标记为1

③这时候再插入元素C

HashFun1(C) = 位置1

HashFun2(C) = 位置3

HashFun3(C) = 位置13

13号位置没有被占据,说明这个元素没有插入过

那么将这位置13对应位置标记为1

④这时候如果插入元素A

HashFun1(A) = 位置1

HashFun2(A) = 位置2

HashFun3(A) = 位置15

可以发现三个位置都为1,则元素A有可能插入过了

⑤这时候如果插入元素D

HashFun1(A) = 位置1

HashFun2(A) = 位置2

HashFun3(A) = 位置14

可以发现三个位置都为1,则元素D有可能插入过了

从上面可以发现:
判断"元素不在集合中"时准确率是100%(不存在误判)
判断"元素在集合中"时,可能存在误判(准确率不是100%)

最简单的实现(不考虑失误率):

复制代码
#include <iostream>
#include <vector>
#include <string>

class SimpleBloomFilter {
private:
    std::vector<uint8_t> bit_array;  // 使用char代替bool以避免vector<bool>的问题
    int size;                     // 位数组大小
    int hash_func_count;          // 哈希函数数量

public:
    // 构造函数
    SimpleBloomFilter(int bit_array_size = 1024, int num_hash_func = 3) 
        : size(bit_array_size), hash_func_count(num_hash_func) {
        bit_array.resize(size, 0);  // 初始化size大小的容器,初始化为0
    }

    // 添加元素
    void add(const std::string& item) {
        std::hash<std::string> hasher;
        for (int i = 0; i < hash_func_count; ++i) {
            // 使用不同的种子创建不同的哈希值,模拟使用了不同的哈希函数
            size_t hash = hasher(item + std::to_string(i));
            size_t index = hash % size;
            bit_array[index] = 1;
        }
    }

    // 检查元素是否存在
    bool contains(const std::string& item) const {
        std::hash<std::string> hasher;
        for (int i = 0; i < hash_func_count; ++i) {
            size_t hash = hasher(item + std::to_string(i));
            size_t index = hash % size;
            if (bit_array[index] == 0) {
                return false;  // 如果有一位为0,则肯定不存在
            }
        }
        return true;  // 所有位都为1,可能存在(可能有误报)
    }
};

int main() {
    // 创建一个简单的布隆过滤器
    SimpleBloomFilter bf;

    // 添加一些元素
    bf.add("apple");
    bf.add("banana");
    bf.add("orange");

    // 测试元素是否存在
    std::cout << std::boolalpha;  // 输出true/false而非1/0
    std::cout << "Contains 'apple': " << bf.contains("apple") << std::endl;     // true
    std::cout << "Contains 'banana': " << bf.contains("banana") << std::endl;   // true
    std::cout << "Contains 'watermelon': " << bf.contains("watermelon") << std::endl; // 可能false或误报true
    std::cout << "Contains 'strawberry': " << bf.contains("strawberry") << std::endl;       // 可能false

    return 0;
}

说明:

①这里注意使用std::vector<uint8_t>替代std::vector<bool>,因为std::vector<bool>有一定的争议,我可能会在后面的文章中说明这个"奇怪"的东西

②size_t hash = hasher(item + std::to_string(i)); 这里通过添加不同的后缀,可以模拟多个不同的哈希函数

误判率函数推导:(不想看推导可直接套用最后算出的结论)

  • 位数组(Bit Array)大小:m

  • 哈希函数数量:k

  • 集合元素数量:n(插入n个元素)

对于某个特定位置的比特位,一次哈希未选中的概率为

对于某个特定位置的比特位,K次哈希未选中的概率为

对于某个特定位置的比特位,插入K次后未被选中的概率为

我们有

设x = 1/m推出对于某个特定位置的比特位,插入K次后未被选中的概率为

则对于某个特定位置的比特位,插入K次后被选中的概率为

则对于K个特定位置的比特位,插入K次后被选中的概率为

为了使误判率最低,则要求P的最低值,后的计算比较繁琐了,

经过复杂计算,这里的最低点是,即函数数量为K的时候误判率最低

把这个带入误报率公式P中

求出

考虑失误率的实现:

复制代码
#include <iostream>
#include <vector>
#include <string>
#include <cmath>

class BloomFilter {
private:
    std::vector<uint8_t> bit_array;  // 使用uint8_t代替bool
    int size;                     // 位数组大小
    int hash_func_count;          // 哈希函数数量

public:
    // 构造函数
    // expected_num_items: 预期要存储的元素数量
    // false_positive_prob: 期望的误报率
    BloomFilter(int expected_num_items, double false_positive_prob) {
        // 计算最优的位数组大小和哈希函数数量
        size = calculate_size(expected_num_items, false_positive_prob);
        hash_func_count = calculate_hash_count(size, expected_num_items);
        
        // 初始化位数组 (每个uint8_t存储8位)
        bit_array.resize((size + 7) / 8, 0);
        
        std::cout << "Bloom Filter initialized with size " << size 
                  << " and " << hash_func_count << " hash functions." << std::endl;
    }

    // 添加元素
    void add(const std::string& item) {
        std::hash<std::string> hasher;
        for (int i = 0; i < hash_func_count; ++i) {
            size_t hash = hasher(item + std::to_string(i));
            size_t index = hash % size;
            // 设置对应的位
            bit_array[index / 8] |= (1 << (index % 8));
        }
    }

    // 检查元素是否存在
    bool contains(const std::string& item) const {
        std::hash<std::string> hasher;
        for (int i = 0; i < hash_func_count; ++i) {
            size_t hash = hasher(item + std::to_string(i));
            size_t index = hash % size;
            // 检查对应的位
            if (!(bit_array[index / 8] & (1 << (index % 8)))) {
                return false;
            }
        }
        return true;
    }

private:
    // 计算位数组大小
    int calculate_size(int n, double p) {
        // m = -(n * ln(p)) / (ln(2)^2)
        double m = -(n * log(p)) / (log(2) * log(2));
        return static_cast<int>(m);
    }

    // 计算哈希函数数量
    int calculate_hash_count(int m, int n) {
        // k = (m/n) * ln(2)
        double k = (static_cast<double>(m) / n) * log(2);
        return static_cast<int>(k);
    }
};

int main() {
    // 创建一个布隆过滤器,预期存储1000个元素,误报率为1%
    BloomFilter bf(1000, 0.01);

    // 添加一些元素
    bf.add("apple");
    bf.add("banana");
    bf.add("orange");

    // 测试元素是否存在
    std::cout << std::boolalpha;  // 输出true/false而非1/0
    std::cout << "Contains 'apple': " << bf.contains("apple") << std::endl;     // true
    std::cout << "Contains 'banana': " << bf.contains("banana") << std::endl;   // true
    std::cout << "Contains 'watermelon': " << bf.contains("watermelon") << std::endl; // 可能false或误报true
    std::cout << "Contains 'strawberry': " << bf.contains("strawberry") << std::endl;       // 可能false


    return 0;
}

这里可以根据预期要存储的元素数量和预期的失误率,利用推导出的公式,自动计算出最优的数位组大小和哈希函数数量