BloomFilter是什么?

Bloom filter是由Howard Bloom在1970年提出的二进制向量数据结构,它具有空间和时间效率,被用来检测一个元素是不是集合中的一个成员。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。

布隆过滤器 - 百度百科

优点

  • 空间效率和查询时间都远远超过一般的算法。

它的插入和查询时间都是常数

  • 良好的安全性

它查询元素却不保存元素本身,具有良好的安全性。

缺点

  • 有一定的误识别率

可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。

  • 删除困难

一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。

原理

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。
布隆过滤器(BloomFilter) - 图1

实现参数

布隆过滤器有许多实现与优化,Guava中就提供了一种Bloom Filter的实现。

在使用bloom filter时,绕不过的两点是预估数据量n以及期望的误判率fpp
在实现bloom filter时,绕不过的两点就是hash函数的选取以及bit数组的大小
对于一个确定的场景,我们预估要存的数据量为n,期望的误判率为fpp,然后需要计算我们需要的Bit数组的大小m,以及hash函数的个数k,并选择hash函数

Bit数组大小选择

根据预估数据量n以及误判率fpp,bit数组大小的m的计算方式:
布隆过滤器(BloomFilter) - 图2

哈希函数选择

由预估数据量n以及bit数组长度m,可以得到一个hash函数的个数k:
布隆过滤器(BloomFilter) - 图3
哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。

哈希函数个数k、位数组大小m、加入的字符串数量n的关系可以参考Bloom Filters - the mathBloom_filter-wikipedia

自己实现

  1. public class MyBloomFilter {
  2. /**
  3. * 你的布隆过滤器容量
  4. */
  5. private static final int DEFAULT_SIZE = 2 << 28;
  6. /**
  7. * bit数组,用来存放key
  8. */
  9. private static BitSet bitSet = new BitSet(DEFAULT_SIZE);
  10. /**
  11. * 后面hash函数会用到,用来生成不同的hash值,可随意设置,别问我为什么这么多8,图个吉利
  12. */
  13. private static final int[] INTS = {1, 6, 16, 38, 58, 68};
  14. public static void main(String[] args) {
  15. MyBloomFilter myNewBloomFilter = new MyBloomFilter();
  16. myNewBloomFilter.add("张学友");
  17. myNewBloomFilter.add("郭德纲");
  18. myNewBloomFilter.add("蔡徐鸡");
  19. myNewBloomFilter.add(666);
  20. System.out.println(myNewBloomFilter.isContain("张学友"));//true
  21. System.out.println(myNewBloomFilter.isContain("张学友 "));//false
  22. System.out.println(myNewBloomFilter.isContain("张学友1"));//false
  23. System.out.println(myNewBloomFilter.isContain("郭德纲"));//true
  24. System.out.println(myNewBloomFilter.isContain("蔡徐老母鸡"));//false
  25. System.out.println(myNewBloomFilter.isContain(666));//true
  26. System.out.println(myNewBloomFilter.isContain(888));//false
  27. }
  28. /**
  29. * add方法,计算出key的hash值,并将对应下标置为true
  30. */
  31. public void add(Object key) {
  32. Arrays.stream(INTS).forEach(i -> bitSet.set(hash(key, i)));
  33. }
  34. /**
  35. * 判断key是否存在,true不一定说明key存在,但是false一定说明不存在
  36. */
  37. public boolean isContain(Object key) {
  38. boolean result = true;
  39. for (int i : INTS) {
  40. //短路与,只要有一个bit位为false,则返回false
  41. result = result && bitSet.get(hash(key, i));
  42. }
  43. return result;
  44. }
  45. /**
  46. * hash函数,借鉴了hashmap的扰动算法,强烈建议大家把这个hash算法看懂,这个设计真的牛皮加闪电
  47. */
  48. private int hash(Object key, int i) {
  49. int h;
  50. return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16)));
  51. }
  52. }
  1. true
  2. false
  3. false
  4. true
  5. false
  6. true
  7. false

自己实现这边只使用了一次hash函数,实际实现的时候可能会使用到多次hash函数。

guava实现

引入guava包

  1. <dependency>
  2. <groupId>com.google.guava</groupId>
  3. <artifactId>guava</artifactId>
  4. <version>29.0-jre</version>
  5. </dependency>

测试类

测试分两步:
1、往过滤器中放一百万个数,然后去验证这一百万个数是否能通过过滤器
2、另外找一万个数,去检验漏网之鱼的数量

public class TestBloomFilter {

    /**
     * 布隆过滤器的大小
     */
    private static int total = 1000000;
    /**
     * 布隆过滤器
     */
    private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total);
    public static void main(String[] args) {
        // 初始化1000000条数据到过滤器中
        for (int i = 0; i < total; i++) {
            bf.put(i);
        }

        // 匹配已在过滤器中的值,是否有匹配不上的
        for (int i = 0; i < total; i++) {
            if (!bf.mightContain(i)) {
                System.out.println("有坏人逃脱了~~~");
            }
        }

        // 匹配不在过滤器中的10000个值,有多少匹配出来
        int count = 0;
        for (int i = total; i < total + 10000; i++) {
            if (bf.mightContain(i)) {
                count++;
            }
        }
        System.out.println("误伤的数量:" + count);
    }

}

输出

误伤的数量:320

运行结果表示,遍历这一百万个在过滤器中的数时,都被识别出来了。一万个不在过滤器中的数,误伤了320个,错误率是0.03左右。

错误率的选择

看下BloomFilter的源码:

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions, double fpp) {
    return create(funnel, (long)expectedInsertions, fpp);
}

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions, double fpp) {
    return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
}

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

    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);

    try {
        return new BloomFilter(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
    } catch (IllegalArgumentException var10) {
        throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", var10);
    }
}

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions) {
    return create(funnel, (long)expectedInsertions);
}

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
    return create(funnel, expectedInsertions, 0.03D);
}

该版本的BloomFilter一共是5个create方法,最终都走向了第三个create方法。

  • 看一下每个参数的含义:
  • funnel:数据类型(一般是调用Funnels工具类中的)
  • expectedInsertions:期望插入的值的个数,Long类型,取值范围大于等于0。
  • fpp 错误率(默认值为0.03),Double类型,取值范围在0-1之间。
  • strategy 哈希算法(我也不懂啥意思)Bloom Filter的应用

在最后一个create方法中,设置一个断点:
image.png

当我们使用这种没有指定错误率的方式创建的时候

private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total);

image.png
上面的numBits,表示存一百万个int类型数字,需要的位数为7298440,700多万位。理论上存一百万个数,一个int是4字节32位,需要4_8_1000000=3200万位。如果使用HashMap去存,按HashMap50%的存储效率,需要6400万位。可以看出BloomFilter的存储空间很小,只有HashMap的1/10左右。
上面的numHashFunctions,表示需要5个函数去存这些数字。

当我们使用指定错误率的方式创建的时候

错误率设置为0.0003

private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total, 0.0003);
误伤的数量:4

此时误伤的数量为4,错误率为0.000004左右。
image.png
当错误率设为0.0003时,所需要的位数为16883499,1600万位,需要12个函数
和上面对比可以看出,错误率越大,所需空间和时间越小,错误率越小,所需空间和时间约大

使用场景

  • cerberus在收集监控数据的时候, 有的系统的监控项量会很大, 需要检查一个监控项的名字是否已经被记录到db过了, 如果没有的话就需要写入db.
  • 爬虫过滤已抓到的url就不再抓,可用bloom filter过滤
  • 垃圾邮件过滤。如果用哈希表,每存储一亿个 email地址,就需要 1.6GB的内存(用哈希表实现的具体办法是将每一个 email地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email地址需要占用十六个字节。一亿个地址大约要 1.6GB,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百 GB的内存。而Bloom Filter只需要哈希表 1/8到 1/4 的大小就能解决同样的问题。 :::info 以上这些场景有个共同的问题:如何查看一个东西是否在有大量数据的池子里面。 :::