最近做爬虫项目过滤重复的url的时候,了解到一个东西,叫布隆过滤器,然后也学习了一下,写下这篇博客记录一下.
下面我们将分为几个专题来介绍布隆过滤器:
1.什么是布隆过滤器;
2.布隆过滤器的使用场景和缺陷;
3.布隆过滤器java实现;
4.guava中使用布隆过滤器;
5.布隆过滤器的变体

1.什么是布隆过滤器?

    首先我们得知道布隆过滤器的概念是什么,采自wiki百科:
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
tips:看完这个我们可以知道是一个叫布隆的人提出的一个用来检索一个元素是否在一个集合中的算法,效率高,性能好。

1.1 图解布隆过滤器

很长的二进制向量(这里可以理解为很长的bit数组)
image
一系列的随机映射函数(hash函数)
image

    如图所示,将一个字符串存入布隆过滤器的时候,这个字符串会先被多个hash函数生成不同的hash值,然后在对应的bit数组的位置,将0置为1(bit数组初始化的时候,全部位置都是0);然后第二次在有相同的字符串存入的时候,因为之前已经对应的位置都被置为1了,所以可以很轻松的知道这个值已经存在了。
    举个栗子,比如第一次将abc@gmail.com存入布隆过滤器,将bit数组的1,3,5位置置为了1,只要下次再有abc@gmail.com存入布隆过滤器,发现1,3,5已经是全是1了,所以可知该字符串已经保存过。(简单来说就是bit数组中对应的值只要全是1就存在,其他情况就是不存在,但是因为存在hash冲突,所以会有误判,有可能存在abc和xyz的hash值在bit数组中映射的位置是相同的。这种情况我们可以增加班名单,或者调整hash函数来减少误判情况。)

2.布隆过滤器的使用场景

知道了布隆过滤器的概念,我们再来看看在实际工作中,它主要使用在哪些地方。

2.1使用场景:

1.网络爬虫可以通过布隆过滤器判断当前的url是否已经爬取过;

2.防止恶意链接或者垃圾邮件,短信之类的,从数十亿个链接或者垃圾邮件中判断该链接(邮件发件人,短信发信人是否是在黑名单中),
平时手机上来电提示写着对方式恶意推销,外卖,这种场景也是可以用布隆过滤器来判断;

3.防止缓存击穿,将已存在的缓存放到布隆中,当使用缓存的时候,可以先访问布隆过滤器,存在则访问缓存,不存在则访问数据库;

4.检索系统查询当前的输入信息是否存在于数据库中,也可以使用布隆过滤器。

3.布隆过滤器java实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

public class BloomFilter {


/**
* bitSet的大小
*/
private static final int DEFAULT_SIZE = 2 << 24;
/**
* 选取的hash函数
*/
private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};

/**
* bitSet每一位只能是true或false 其实就是bit数组说的0或者1
*/
private BitSet bits = new BitSet(DEFAULT_SIZE);
private SimpleHash[] func = new SimpleHash[SEEDS.length];

public static void main(String[] args) {
String value = "wxwwt@gmail.com";
BloomFilter filter = new BloomFilter();
System.out.println(filter.contains(value));
filter.add(value);
System.out.println(filter.contains(value));
}

public BloomFilter() {
for (int i = 0; i < SEEDS.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
}
}

public void add(String value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}

public boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}

public static class SimpleHash {

private int cap;
private int seed;

public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}

/**
* 计算hash值
*
* @param value
* @return
*/

public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
return (cap - 1) & result;
}

}
}

4.使用guava带的布隆过滤器

google的java工具包中已经编写了布隆过滤器的代码,可以直接拿来用,具体使用可以google一下,这里只简单的提一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
// 创建布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 1000);

// 添加数据
for (int index = 0; index < 100000; index++) {
bloomFilter.put("wxwwt-" + index);
}

// 查看数据是否存在
if (bloomFilter.mightContain("wxwwt-" + 9999)) {
System.out.println("存在");
}
// 误判元素
if (bloomFilter.mightContain("不存在的元素")) {
System.out.println("误判啦");
} else {
System.out.println("不存在");
}
}

运行结果:
img

5.总结:

在数据量很大的时候使用布隆过滤器非常方便,占用的内存空间很小(因为使用的是bit数组,空间使用非常小,空间开销就是bit数组的大小),查询效率也很高(直接通过计算hash函数的出来的),唯一的问题就是可能会有误判,不过概率也是比较小的,也可以通过增加白名单和增加hash函数的数量来减少这个问题的产生,总的来说利大于弊,在仅判断元素是否存在而不涉及到删除的情况下非常好用(最基本的bloomfilter是无法删除元素的,置为0就没法判断存在情况了,有bloom过滤器的变体是支持删除的)。

参考资料:

1.https://nick-weixx.github.io/2018/03/01/guava-bloomfilter_1/
2.https://zh.wikipedia.org/wiki/%E5%B8%83%E9%9A%86%E8%BF%87%E6%BB%A4%E5%99%A8
3.https://zhangluncong.com/2018/05/23/bloomFilter/