布隆过滤器

什么是布隆过滤器(Bloom Filter)

1.引言
上一篇博客: Redis缓存雪崩、缓存击穿、缓存穿透
在上一篇博客中,我们了解了Redis缓存雪崩、缓存击穿、缓存穿透,并且知道了解决缓存穿透可以使用布隆过滤器,接下来我们就一起来看看这个布隆过滤器吧!

2.布隆过滤器

首先,我们需要了解布隆过滤器的概念。
布隆过滤器(Bloom Filter)是一个叫做 Bloom 的老哥于1970年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
如果想要判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。

布隆过滤器的原理

布隆过滤器
每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。我们用不同位置的1来组合成不同的数据。

当我们向布隆过滤器中添加一个元素的时候,布隆过滤器是这样将它存储的:
第一步、使用哈希函数对元素进行计算 ,然后得到哈希值。(有几个哈希函数就会得到几个值)。
第二步、根据得到的哈希值,在位数组中把对应下标的值置为 1。

如图所示:
在布隆过滤器中存储 hello 这个数据,那么首先会计算hello 的哈希函数值, 然后算出来对应的下标值, 然后把对应的值改为1即可。(注意,最开始的时候数组里所有元素均为0)

布隆过滤器

注意这里不一定必须是三个hash函数,这个可以自己设置的,待会我们就用java语言来模拟一下这个过程。

如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

不同的字符串可能通过哈希函数计算的出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数

布隆过滤器使用场景

1.首先肯定是解决Redis缓存穿透问题。(判断请求是否无效)
2.在海量元素中(例如 10 亿无序、不定长、不重复)快速判断一个元素是否存在。
3.识别恶意 URL;
4.从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
。。。

布隆过滤器缺陷

布隆过滤器有一个缺陷,就是误判率。
什么叫误判率?
误判率是指当我们使用布隆过滤器查询一个数据时,结果返回的都是1,则说明它存在对吧。这样的话就错了!它有可能不存在,因为有可能其他数据的哈希结果也是这样。

我们可以总结一下:

如果布隆过滤器判断元素在集合中存在,不一定存在;
如果布隆过滤器判断不存在,一定不存在;
如果元素实际存在,布隆过滤器一定判断存在
如果元素实际不存在,布隆过滤器可能判断存在

3.java实现

接下来我们就用Google开源的Guava来实现一下布隆过滤器。

第一步、引入guava依赖

<dependency> 
          <groupId>com.google.guava</groupId> 
          <artifactId>guava</artifactId> 
          <version>21.0</version> 
      </dependency>

第二步

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterTest {
     //插入10000条数据数据
    private  static  final  int NUMBER = 100000;
    //误判率
    private static double fpp = 0.02;

    public static void main(String[] args) {
        //初始化布隆过滤器,默认误判率 0.02
        BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), NUMBER, fpp);
        // 判断指定元素是否存在
        System.out.println(bf.mightContain(1));
        System.out.println(bf.mightContain(2));
        // 将元素添加进布隆过滤器
        bf.put(1);
        bf.put(2);
        System.out.println(bf.mightContain(1));
        System.out.println(bf.mightContain(2));


    }
}

输出结果:

false
false
true
true

方法返回true时,我们可以99%确定该元素在过滤器中,当过滤器返回false时,我们可以100%确定该元素不存在于过滤器中。

下面我们来改造一下代码,来看看误判率:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterTest {
     //插入10000条数据数据
    private  static  final  int NUMBER = 100000;
    //误判率
    private static double fpp = 0.02;

    public static void main(String[] args) {
        //初始化布隆过滤器,默认误判率 0.02
        BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), NUMBER, fpp);

        //插入10w样本数据
        for (int i = 0; i <NUMBER ; i++) {
            bf.put(i);
        }
        int count = 0;
        //用另外10w的数据来测试误判率
        for (int i = NUMBER; i < NUMBER + 100000; i++) {
             if(bf.mightContain(i)) {
                 count++;
                 System.out.println(i+"误判");
             }

        }
        System.out.println("总共的误判:"+count);
    }
}

结果:
布隆过滤器

误判了1924次,来算一下,1924 / 100000 约等于 0.02 ,嗯。跟想的一样。

这样得出一个结论:
虽然我们可以手动设置误判率,但是要设置合理。误判率越小,那么误判的结果就越少。 但是误判率越小,hash计算的时间就越来越长,所需要的Hash函数也越多,性能也就越差。

上一篇:基于C++实现LZW编解码算法


下一篇:Redis——深入得学习与源码分析:布隆过滤器