在上篇文章海量数据下的去重和查重(一):BitMap位图法的最后,我们说到位图法缺点,是其所占空间随集合内最大元素的增大而增大。这就会带来一个问题,如果查找的元素数量少但其中某个元素的值很大,比如数字范围是 1 到 1000 亿,那消耗的空间不容乐观。
针对这个缺点,有一种改进是布隆过滤器。
布隆过滤器
所谓布隆过滤器,实际上就是一个位图bitMap,我们现在假设有一个长度为m的bit类型的数组,以及 K 个互相独立的优秀的哈希函数,且这k个哈希函数的输出域都大于或等于 m。
当一个元素加入布隆过滤器中的时候,会进行如下操作:
- 使用 K 个哈希函数对元素值进行 K 次计算,得到 K 个哈希值,我们将这K个值对m模运算,得到的 k 个值都在 0~m之间
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
当要判断一个值是否在布隆过滤器中,对元素再次进行哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
布隆过滤器最大的用处,就是可以用来判断一个元素是否在一个集合中,很常用的一个功能是用来去重。比如可以用来解决下面的需求
需求一:不安全网页的黑名单包含100亿个黑名单网页,每个网页的URL最多占用64B,现在要实现一个网页过滤系统,根据网页的URL判断该网页是否在黑名单上。我们可以把黑名单url加载到数据库或者redis中,但是数据库存在效率问题,redis存在内存问题。
需求二:在爬虫中,目标网站 URL 千千万,怎么判断某个 URL 爬虫是否宠幸过?简单点可以爬虫每采集过一个 URL,就把这个 URL 存入数据库中,每次一个新的 URL 过来就到数据库查询下是否访问过。
但是随着爬虫爬过的 URL 越来越多,每次请求前都要访问数据库一次,并且对于这种字符串的 SQL 查询效率并不高。除了数据库之外,使用 Redis 的 set 结构也可以满足这个需求,并且性能优于数据库。而Redis 也存在一个问题:耗费过多的内存。
对于上述两个问题,相比于数据库和 Redis,使用布隆过滤器可以很好的避免性能和内存占用的问题。
特点——宁可错杀三千,绝不放过一个
从上面的介绍中可以看出,当插入的元素越来越多,位数组中被置为 1 的位置就越多,当一个不在布隆过滤器中的元素,经过哈希计算之后,得到的值在位数组中查询,有可能这些位置也都被置为 1。这样一个不存在布隆过滤器中的也有可能被误判成在布隆过滤器中。但是如果布隆过滤器判断说一个元素不在布隆过滤器中,那么这个值就一定不在布隆过滤器中。简单来说:
布隆过滤器说某个元素在,可能会被误判。
布隆过滤器说某个元素不在,那么一定不在。
这个布隆过滤器的缺陷放到上面爬虫的需求中,可能存在某些没有访问过的 URL 可能会被误判为访问过,但是如果是访问过的 URL 一定不会被误判为没访问过。在黑名单需求中,一个正常url可能会被误判为是黑名单url,从而被拦截,但是一个黑名单url是肯定会被拦截的。
虽然存在这个问题,但是在这两种需求中,相对于性能和内存来说,一点点的误判率是可以接受的。
常见的补救办法是建立白名单,存储那些可能被误判的元素。 比如你苦等的offer 可能被系统丢在邮件垃圾箱(白名单)了。
使用场景
布隆过滤器的最大的用处就是,能够迅速判断一个元素是否在一个集合中。因此它有如下三个使用场景:
- 网页爬虫对 URL 的去重,避免爬取相同的 URL 地址
- 进行垃圾邮件过滤:反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)
- 有的黑客为了让服务宕机,他们会构建大量不存在于缓存中的 key 向服务器发起请求,在数据量足够大的情况下,频繁的数据库查询可能导致 DB 挂掉。布隆过滤器很好的解决了缓存击穿的问题,通过布隆过滤器
将所有的可用的key放入缓存,当请求进来时,先去过滤器中校验是否存在,如果不存在直接返回null。
项目中使用
1、redis布隆过滤器
com.redislabs
jrebloom
1.0.2
public class RedisBloomDemo {
public static void main(String[] args) {
String userIdBloomKey = "userid";
// 创建客户端,jedis实例
Client client = new Client("localhost", 6378);
// 创建一个有初始值和出错率的过滤器
client.createFilter(userIdBloomKey,100000,0.01);
// 新增一个
boolean userid1 = client.add(userIdBloomKey,"101310222");
System.out.println("userid1 add " + userid1);
// 批量新增values
boolean[] booleans = client.addMulti(userIdBloomKey, "101310111", "101310222", "101310222");
System.out.println("add multi result " + booleans);
// 某个value是否存在
boolean exists = client.exists(userIdBloomKey, "101310111");
System.out.println("101310111 是否存在" + exists);
//某批value是否存在
boolean existsBoolean[] = client.existsMulti(userIdBloomKey, "101310111","101310222", "101310222","11111111");
System.out.println("某批value是否存在 " + existsBoolean);
}
}
2、Guava中的BloomFilter
com.google.guava
guava
22.0
private static int size = 1000000;
private static BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), size, 0.0001);
public void test2() {
String aa = "zjl";
bloomFilter.put(aa);
System.out.println(bloomFilter.mightContain(aa));
}