在学习Redis时,三大缓存问题是不可避免的,所以,我也是在此学习到了布隆过滤器?于是总结了这篇文章。
一、什么是布隆过滤器?
布隆过滤器是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
说到检索一个元素是否在一个集合中,我们可能想到Java中的HashMap这些,但为什么不用这些来做呢?其实HashMap 的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,如果上亿数据时候,那 HashMap 占据的内存大小就变得很不可观了。
当一个元素加入布隆过滤器中的时候,会进行两步:
第一步:使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
第二步:根据得到的哈希值,在位数组中把对应下标的值置为 1。
如下图,添加了两个字符串,但是要注意的是,5这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。
当我们需要判断一个元素是否存在于布隆过滤器的时候,也会进行两步:
第一步:对给定元素再次进行相同的哈希计算;
第二步:得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值可能在布隆过滤器中,如果存在一个值不为 1,说明该元素一定不在布隆过滤器中。
如果现在要判断听风逝夜,计算出的结果为2、6、7,可其中有两位为0,则表示这数据一定不存在。但如果计算结果为1、3、4,可这几位都为1,能说明存在吗?答案是不能,只能说明可能存在。原因是因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断这个值存在。
二、布隆过滤器使用场景
- 判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。
- 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。
三、布隆过滤器实现
我们可以利用Google开源的 Guava中自带的布隆过滤器来做。Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
public static void main(String[] args) {
// 创建布隆过滤器对象
int size =100000;
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), size);
for (int i = 1; i <=size; i++) {
filter.put(i);
}
int count =0;
for (int i = (size+1); i <=size*2; i++) {
boolean b = filter.mightContain(i);
if (b){
count++;
}
}
DecimalFormat df=new DecimalFormat("0.00");
System.out.println("错判率为:" + df.format((float)count/10000));
}
运行后我们发现,过滤器有 3% 的错判率,通过源码阅读,发现 3% 的错判率是系统写死的。
当然我们也可以通过传参,降低错判率。测试了一下,查询速度稍微有一丢丢降低,但也只是零点几毫秒级的而已。
static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy);
// 参数含义:
// funnel 指定布隆过滤器中存的是什么类型的数据,有:IntegerFunnel,LongFunnel,StringCharsetFunnel等。
// expectedInsertions 预期需要存储的数据量
// fpp 误判率,默认是0.03。
四、什么不支持删除
可以举一个例子来说明。
比如要删除集合中的成员”听风“,那么就会先用 k个哈希函数对其计算,因为”听风“已经是集合成员,那么在位数组的对应位置一定是 1,我们如要要删除这个成员”听风“,就需要把计算出来的所有位置上的 1 置为 0,也就是将1、3 和 5 位置为 0 。但是,“逝夜”也是属于集合的元素,如果需要查询 “逝夜” 是否在集合中,通过哈希函数计算后,我们会去判断2、4、5位是否为 1, 这时候就得到了第5位为0的结果,即“逝夜”不属于集合。 显然这里是误判的。
但是Counting Bloom Filter可以了解一下