Bpazy / blog

我的博客,欢迎关注和讨论
https://github.com/Bpazy/blog/issues
MIT License
41 stars 2 forks source link

布隆过滤器 #302

Open Bpazy opened 1 year ago

Bpazy commented 1 year ago

布隆过滤器(Bloom Filter)是由布隆(Burton Howard Bloom)在 1970 年提出的,它实际上是由一个很长的二进制向量和一系列随机hash映射函数组成(说白了,就是用二进制数组存储数据的特征)。

譬如下面例子:有三个hash函数,那么“陈六”就会被三个hash函数分别hash,并且对位数组的长度,进行取余,分别hash到三个位置。 image

Bpazy commented 1 year ago

布隆过滤器的误判

误判率的近似公式: image 其中 k 为 hash 函数个数,n 为数字个数。

如何确定最优的m和k?

知道原理后再来了解下怎么去实现,我们在决定使用Bloomfilter之前,需要知道两个数据,一个是要存储的数量n和预期的误判率p。bitmap的大小m决定了存储空间的大小,hash函数个数k决定了计算量的大小,我们当然都希望m和k都越小越好,如何计算二者的最优值,我们大概来推导下。(备注:推导过程来自Wikipedia)

由上文可知,误判率p为 image

对于给定的m和n我们想让误判率p最小,就得让 image

把(2)式代入(1)中可得 image

对(3)两边同时取ln并简化后,得到 image

最后可以计算出m的最优值为 image

因为误判率p和要插入的数据量n是已知的,所以我们可以直接根据上式计算出m的值,然后把m和n的值代回到(2)式中就可以得到k的值。至此我们就知道了实现一个bloomfilter所需要的所有参数了,接下来让我们看下Google guava包中是如何实现BloomFilter的。

guava中的BloomFilter

BloomFilter无法通过new去创建新对象,而它提供了create静态方法来生成对象,其核心方法如下。

  static <T> BloomFilter<T> create(
      Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
    checkNotNull(funnel);
    checkArgument(
        expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
    checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
    checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
    checkNotNull(strategy);

    if (expectedInsertions == 0) {
      expectedInsertions = 1;
    }

    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    try {
      return new BloomFilter<T>(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
    }
  }

从代码可以看出,需要4个参数,分别是:

从上面代码可知,BloomFilter创建过程中先检查参数的合法性,之后使用n和p来计算bitmap的大小m(optimalNumOfBits(expectedInsertions, fpp)),通过n和m计算hash函数的个数k(optimalNumOfHashFunctions(expectedInsertions, numBits)),这俩方法的具体实现如下。

  static int optimalNumOfHashFunctions(long n, long m) {
    // (m / n) * log(2), but avoid truncation due to division!
    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
  }
  static long optimalNumOfBits(long n, double p) {
    if (p == 0) {
      p = Double.MIN_VALUE;
    }
    return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
  }

参考:

  1. https://developer.aliyun.com/article/743706
  2. https://en.wikipedia.org/wiki/Bloom_filter
Bpazy commented 1 year ago

使用案例

Bpazy commented 1 year ago

各种布隆过滤器实现的对比

内存消耗情况

基于 1000W 随机手机号测试

set集合 guava布隆过滤器(expectedInsertions = 1000_0000,fpp = 0.00001) guava布隆过滤器(expectedInsertions = 1000_0000,fpp = 0.000001) guava布隆过滤器(expectedInsertions = 1000_0000,fpp = 0.0000001) hutool布隆过滤器(bitmap大小:10M) hutool布隆过滤器(bitmap大小:30M) hutool布隆过滤器(bitmap大小:50M) hutool布隆过滤器(bitmap大小:100M)
内存消耗 996M 30M 35M 40M 10M 30M 50M 100M
误判数 0 5 0 0 400290 24973 3672 447
误判率 0 0.000002 0 0 0.040029 0.0024973 0.0003672 0.0000447

性能测试

读取文件内容 set集合 guava布隆过滤器(expectedInsertions = 100_0000,fpp = 0.00001) guava布隆过滤器(expectedInsertions = 100_0000,fpp = 0.000001) guava布隆过滤器(expectedInsertions = 500_0000,fpp = 0.00001) guava布隆过滤器(expectedInsertions = 500_0000,fpp = 0.000001) guava布隆过滤器(expectedInsertions = 1000_0000,fpp = 0.00001) guava布隆过滤器(expectedInsertions = 1000_0000,fpp = 0.000001) hutool布隆过滤器(bitmap大小:10M) hutool布隆过滤器(bitmap大小:30M) hutool布隆过滤器(bitmap大小:50M) hutool布隆过滤器(bitmap大小:100M)
100W数据 105ms 350ms 994ms 1086ms 1343ns 1500ms 1405ms 1586ms 502ms 596ms 618ms 649ms
500W数据 306ms 1921ms 2795ms 3559ms 5481ms 6569ms 6201ms 6973ms 2159ms 2662ms 2880ms 3047ms
10000W数据 487ms 6848ms 4753ms 5832ms 9776ms 11580ms 11497ms 13398ms 3808ms 5273ms 5572ms 6055ms

总结

  1. 使用方面

guava的布隆过滤器api比较友好,只需要传入预估数据量、误判率,这些都是开发人员很容易评估的。 hutool的布隆过滤器api默认构建方法比较难衡量怎么用,虽然可以优化,但算法不熟的话,很难下手。

  1. 内存、误判率方面

guava的布隆过滤器在预估数据量准确的情况下,误判率可以很低,且内存占用相对较小。 hutool的布隆过滤器(默认构造出来的实例)随着设置的bitmap大小越大,误判率就越低,但相对guava的来说,内存、误判率方面guava表现更佳。

  1. 速度、性能方面

guava的布隆过滤器插入数据耗时相对hutool的耗时较多,可能是hutool默认5个hash函数的原因,而guava的hash函数个数是会根据数量和误判率来变化的。

  1. 最终总结

使用guava更方便,并且在内存节省、误判率上有很好的表现,虽然性能耗时上会多一些,但这点时间对于一个耗时任务来说影响不大。

Bpazy commented 1 year ago

集群中的布隆过滤器

可利用 Redis 来实现布隆过滤器,途径有多种:

  1. 利用 Redis 的 Bitmaps 结构。缺点是算法需要自己实现,布隆过滤器有很多用于解决例如“数据稀疏”问题的变种算法。
  2. 利用 Redis 的插件 RedisBloom,对应的 Java 开源客户端有 JRedisBloom。
  3. 利用 Redisson 的 BloomFilter。
miximixi2 commented 1 year ago

Why three functions 布隆过滤器在查询的时候为什么是三个函数,而不是多个 没有恶意纯属小白