布隆过滤器(Bloom Filter)详解

布隆过滤器是由 Burton Bloom 在 1970 年提出,所以也称为 Bloom Filter。java


  • 做用网页爬虫

    • 判断一个元素是否在一个集合数组


实现缓存

  • 经过一个很长的二进制向量和一系列的随机映射函数实现数据结构

优势app

  • 插入/查询速度快,且有较好的时间和空间效率。时间/空间复杂度都是常量 O(K)dom

缺点ide

  • 有误算率:断定不存在则必定不存在,断定存在则可能实际不存在。随着插入元素数量增长,误算率也增大函数

  • 不能进行删除google

使用场景

  • 防止缓存击穿

  • Web 拦截器

  • 网页爬虫对 URL 去重

  • 反垃圾邮件,判断某个邮箱是不是垃圾邮

  • ......


1. 实现原理

    

当须要判断一个元素是否在某个集合中时,咱们首先想到的是使用集合类(链表、树、散列表等)来实现。可是使用集合类,咱们是直接将全部元素保存在集合中,随着集合中元素数量的增长,集合所须要的存储空间也会线性增加,同时查询速度也会愈来愈慢。

图片

使用数组或列表时,插入项的位置和要插入的值没有对应关系,这样当须要查找某个值时就必须遍历已有的元素;当数组或列表中存在的元素数据不少时,就会影响查询效率。


针对上述数组查询问题,咱们改成使用哈希表。哈希表经过对“值”进行哈希计算,而后模哈希桶大小,获得存放该值的列表索引位置。查询时根据值的哈希值快速定位到存放该值的索引位置,而后在索引位置列表中进行查找该值是否存在,这样缩小了查询范围,提升了查询的效率。由于咱们是将全部值存放在哈希表中,因此当存放值的数量变多时,会占用大量的存储空间,从而影响查询效率或者发生内存溢出。


这时,咱们可使用布隆过滤器(Bloom Filter)。不论是集合类仍是布隆过滤器,都用到了哈希函数(Hash Function),咱们先来看看哈希函数的定义。


哈希函数


哈希函数,也称为散列函数,给定一个输入值 x,那么通过哈希函数计算输出 H(x),这个计算结果咱们称为哈希值或者散列值。哈希函数具备如下特征:


  • 输入 x 能够任意长度

  • 输出结果 H(x) 的长度固定

  • 算 H(x) 过程高效,长度为 n 的 xH(x) 的时间复杂度应为 O(n)

  • 单向散列:当两个散列值不相同时,那么计算它们的输入值必定也不相同

  • 散列碰撞:当两个散列值相同,可是计算它们的输入值不相同时,咱们称这种状况为散列碰撞

  • 匿性:经过 H(x) 结果,不能从计算上逆向推导出 x,不存在比穷举法更好的方法


布隆过滤器数据结构


布隆过滤器(Bloom Filter)本质上是一个长度为m 的位向量或位列表,仅包含二进制的 0 或 1。最初全部的值均为 0

图片

为了减小哈希碰撞,布隆过滤器使用 K 个不一样的哈希函数,并将哈希结果对应的位置为 1

图片

如上图所示:当输入 January 时,经过设置当 3 个哈希函数计算获得索引位置 一、四、6,咱们将这 3 个索引位置置为 1

图片

而后再输入 February,根据哈希函数获得索引位置 一、五、8。由于索引位 1 上已是 1,所以只须要将 五、8 索引位置 1


当查询某个值是否存在时,一样经过设置的哈希函数计算出索引位置,若是相应索引位置上都是 1 则说明该值已存在(存在误判的状况),不然该值必定不存在。


布隆过滤器为何不能删除?如上所示,咱们若是删除 February,则将索引位置 一、五、8 置 0。此时咱们再来判断查询 January 是否存在,由于索引位 1 已经置 0,根据判断规则,则断定 January 不存在。所以在布隆过滤器中删除元素会致使其余已存在元素的误判。


2. Guava Bloom Filter 实现


Google 的 Guava 库中提供了 Bloom Filter 的实现。咱们使用它来实现一个从 1000 万数据中判断随机的 10000 条数据是否存在。


- 引入 guava 库

<dependency>    <groupId>com.google.guava</groupId>    <artifactId>guava</artifactId>    <version>30.1-jre</version></dependency>

- 代码示例

import com.google.common.base.Charsets;import com.google.common.hash.BloomFilter;import com.google.common.hash.Funnels;
import java.util.Random;
public class BloomFilterDemo {
   public static void main(String[] args) {        // 放入 1000 万条数据        int total = 10000000;        BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total);        for (int i = 0; i < total; i++) {            bloomFilter.put("" + i);        }
       // 随机查找 10000 个数        long startTime = System.currentTimeMillis();        int count1 = 0;        int count2 = 0;        for (int i = 0; i < 10000; i++) {            int num = new Random().nextInt(100000000);            if (num < total) {                count1++;            }            String numStr = num + "";            if (bloomFilter.mightContain(numStr)) {                count2++;            }        }        System.out.println("匹配数据:" + count2 + "条,耗时" + (System.currentTimeMillis() - startTime) + "ms");        if (count1 != count2) {            System.out.println("误判:" + Math.abs(count2 - count1) + "条");        }    }
}

- 代码输出

匹配数据:1276条,耗时9ms误判:286条

如上代码所示,布隆过滤器存在误判率,使用 Guava Bloom Filter 时,咱们能够在建立布隆过滤器时设置误判率 FPP 来提升匹配准确度。如将上述代码中 BloomFilter 的建立语句改成:

BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total, 0.0001);

Guava BloomFilter 默认误判率 fpp 为`0.03D`,fpp 越小,匹配精度越高,相应的须要的存储空间越大,因此在实际应用中根据实际业务状况在误判率和存储空间之间选取一个合适的值。