什么是布隆过滤器(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函数也越多,性能也就越差。