Open Bpazy opened 1 year ago
误判率的近似公式: 其中 k 为 hash 函数个数,n 为数字个数。
知道原理后再来了解下怎么去实现,我们在决定使用Bloomfilter之前,需要知道两个数据,一个是要存储的数量n和预期的误判率p。bitmap的大小m决定了存储空间的大小,hash函数个数k决定了计算量的大小,我们当然都希望m和k都越小越好,如何计算二者的最优值,我们大概来推导下。(备注:推导过程来自Wikipedia)
由上文可知,误判率p为
对于给定的m和n我们想让误判率p最小,就得让
把(2)式代入(1)中可得
对(3)两边同时取ln并简化后,得到
最后可以计算出m的最优值为
因为误判率p和要插入的数据量n是已知的,所以我们可以直接根据上式计算出m的值,然后把m和n的值代回到(2)式中就可以得到k的值。至此我们就知道了实现一个bloomfilter所需要的所有参数了,接下来让我们看下Google 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)));
}
参考:
基于 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 |
guava的布隆过滤器api比较友好,只需要传入预估数据量、误判率,这些都是开发人员很容易评估的。 hutool的布隆过滤器api默认构建方法比较难衡量怎么用,虽然可以优化,但算法不熟的话,很难下手。
guava的布隆过滤器在预估数据量准确的情况下,误判率可以很低,且内存占用相对较小。 hutool的布隆过滤器(默认构造出来的实例)随着设置的bitmap大小越大,误判率就越低,但相对guava的来说,内存、误判率方面guava表现更佳。
guava的布隆过滤器插入数据耗时相对hutool的耗时较多,可能是hutool默认5个hash函数的原因,而guava的hash函数个数是会根据数量和误判率来变化的。
使用guava更方便,并且在内存节省、误判率上有很好的表现,虽然性能耗时上会多一些,但这点时间对于一个耗时任务来说影响不大。
可利用 Redis 来实现布隆过滤器,途径有多种:
Why three functions 布隆过滤器在查询的时候为什么是三个函数,而不是多个 没有恶意纯属小白
布隆过滤器(Bloom Filter)是由布隆(Burton Howard Bloom)在 1970 年提出的,它实际上是由一个很长的二进制向量和一系列随机hash映射函数组成(说白了,就是用二进制数组存储数据的特征)。
譬如下面例子:有三个hash函数,那么“陈六”就会被三个hash函数分别hash,并且对位数组的长度,进行取余,分别hash到三个位置。