布隆过滤器(Bloom Filter)
背景
我们以网络爬虫为例。网络间的链接错综复杂,爬虫程序在网络间“爬行”很可能会形成“环”。为了避免形成“环”,程序需要知道已经访问过网站的URL。当程序又遇到一个网站,根据它的URL,怎么判断是否已经访问过呢?
第一个想法就是将已有URL放置在HashSet
中,然后利用HashSet
的特性进行判断。它只花费O(1)的时间。但是,该方法消耗的内存空间很大,就算只有1亿个URL,每个URL只算50个字符,就需要大约5GB内存。
如何减少内存占用呢?URL可能太长,我们使用MD5等单向哈希处理后再存到HashSet中吧,处理后的字段只有128Bit,这样可以节省大量的空间。我们的网络爬虫程序又可以继续执行了。
但是好景不长,网络世界浩瀚如海,URL的数量急速增加,以128bit的大小进行存储也要占据大量的内存。
这种情况下,我们还可以使用BitSet
,使用哈希函数将URL处理为1bit,存储在BitSet中。但是,哈希函数发生冲突的概率比较高,若要降低冲突概率到1%,就要将BitSet
的长度设置为URL个数的100倍。
但是冲突无法避免,这就带来了误判。理想中的算法总是又准确又快捷,但是现实中往往是“一地鸡毛”。我们真的需要100%的正确率吗?如果需要,时间和空间的开销无法避免;如果能够忍受低概率的错误,就有极大地降低时间和空间的开销的方法。
布隆过滤器
布隆过滤器(Bloom Filter)是1970年由布隆提出的。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率。主要应用场景:网页URL的去重,垃圾邮件的判别,集合重复元素的判别,查询加速(比如基于key-value的存储系统)等。
- 初始状态时,Bloom Filter是一个包含m位的位数组(bit-array),初始时候每一位都为0:
- Bloom Filter使用k个相互独立的哈希函数,通过哈希函数将每个元素映射到{1,…,m}的范围中。比如当来了一个元素 a,进行判断,使用2个哈希函数,计算出该元素对应的Hash值为1和5,然后到Bloom Filter中判断第1位和第5位的值,上面全部为0,则a不在Bloom Filter内,将 a 添加进去,也就是将第1位和第5位的值从0修改为1。(布隆过滤器不需要存储元素本身)
- 之后再来的元素,要判断是不是在Bloom Filter内,也是同a一样的方法,通过K个hash函数计算对应的hash值,然后去相应的位上查看是否都是1,只有都是 1 才认为这个元素在集合内,如果有一个为0则不存在,都为1则极为可能存在:
- 随着元素的插入,Bloom filter 中修改的值变多,出现误判的几率也随之变大,当新来一个元素时,满足其在Bloom Filter内的条件,即所有对应位都是 1 ,这样就可能有两种情况,一是这个元素就在集合内,没有发生误判;还有一种情况就是发生误判,出现了哈希碰撞,这个元素本不在集合内。
使用BloomFilter,有三个重要的值,错误率(false positive rate)、哈希函数个数以及BloomFilter位数组的大小,关于这三个值的最优配置有一个原则,(BloomFilter位数组大小)/(实际的元素个数)越大,错误率越低,但消耗的空间会越多。
实现
通过 Java 编程手动实现布隆过滤器
我们上面已经说了布隆过滤器的原理,知道了布隆过滤器的原理之后就可以自己手动实现一个了。
如果你想要手动实现一个的话,你需要:
- 一个合适大小的位数组保存数据
- 几个不同的哈希函数
- 添加元素到位数组(布隆过滤器)的方法实现
- 判断给定元素是否存在于位数组(布隆过滤器)的方法实现。
下面给出一个我觉得写的还算不错的代码(参考网上已有代码改进得到,对于所有类型对象皆适用):
import java.util.BitSet; public class MyBloomFilter { /** * 位数组的大小 */ private static final int DEFAULT_SIZE = 2 << 24; /** * 通过这个数组可以创建 6 个不同的哈希函数 */ private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134}; /** * 位数组。数组中的元素只能是 0 或者 1 */ private BitSet bits = new BitSet(DEFAULT_SIZE); /** * 存放包含 hash 函数的类的数组 */ private SimpleHash[] func = new SimpleHash[SEEDS.length]; /** * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样 */ public MyBloomFilter() { // 初始化多个不同的 Hash 函数 for (int i = 0; i < SEEDS.length; i++) { func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]); } } /** * 添加元素到位数组 */ public void add(Object value) { for (SimpleHash f : func) { bits.set(f.hash(value), true); } } /** * 判断指定元素是否存在于位数组 */ public boolean contains(Object value) { boolean ret = true; for (SimpleHash f : func) { ret = ret && bits.get(f.hash(value)); } return ret; } /** * 静态内部类。用于 hash 操作! */ public static class SimpleHash { private int cap; private int seed; public SimpleHash(int cap, int seed) { this.cap = cap; this.seed = seed; } /** * 计算 hash 值 */ public int hash(Object value) { int h; return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16))); } } }
测试:
String value1 = "https://javaguide.cn/"; String value2 = "https://github.com/Snailclimb"; MyBloomFilter filter = new MyBloomFilter(); System.out.println(filter.contains(value1)); System.out.println(filter.contains(value2)); filter.add(value1); filter.add(value2); System.out.println(filter.contains(value1)); System.out.println(filter.contains(value2));
Output:
false false true true
测试:
Integer value1 = 13423; Integer value2 = 22131; MyBloomFilter filter = new MyBloomFilter(); System.out.println(filter.contains(value1)); System.out.println(filter.contains(value2)); filter.add(value1); filter.add(value2); System.out.println(filter.contains(value1)); System.out.println(filter.contains(value2));
Output:
false false true true
利用Google开源的 Guava中自带的布隆过滤器
在Google Guava library中Google提供了一个布隆过滤器的实现:com.google.common.hash.BloomFilter
https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java
布隆过滤器在大数据下的应用
比如hbase通过rowkey查询时候,使用布隆过滤器快速判断该rowkey是否存在于当前storefile,再比如hive orc结合布隆过滤器,判断某个stripe下是否存在要查询的字段
参考:
https://juejin.im/post/5cc5aa7ce51d456e431adac5
http://lxw1234.com/archives/2015/12/580.htm