什么是布隆过滤器
布隆过滤器是一个由位数组和一系列随机映射函数两部分组成的数据结构。相较于List、Map 、Set
等数据结构,它占用的空间更少并且效率更高,但是缺点是其返回的结果不一定准确,存在误判的可能(类似于Redis中的 HyperLoglog
,不过原理根本不一样==)。理论情况下添加到其中的元素越多,误判的可能性就越大。而且存放在其中的数据不容易删除。
因为位数组中的每个元素都只占用 1 b i t 1 bit 1bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w个元素的位数组只占用 1000000 b i t / 8 = 125000 B y t e = 125000 / 1024 k b ≈ 122 k b 1000000bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 1000000bit/8=125000Byte=125000/1024kb≈122kb 的空间。可见布隆过滤器的空间占用是很小的。
布隆过滤器的原理介绍
当一个元素加入布隆过滤器中的时候,会进行如下操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
- 根据得到的哈希值,在位数组中把对应下标的值置为 1。
当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:
- 对给定元素再次进行相同的哈希计算。
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
举个简单的例子:
如图所示,当将字符串存储到布隆过滤器中时,布隆过滤去首先将该字符串由多个哈希函数生成不同的哈希值,然后在对应的位数组的下标的元素设置为 1。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在。
综上原理我们可以得出:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
布隆过滤器使用场景
- 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中、 防止缓存穿透、邮箱的垃圾邮件过滤、黑名单功能等等。
- 去重:比如对爬虫已经爬取过的 URL 去重。
手写一个布隆过滤器
根据上面讲述的原理我们很容易写出一个简易的布隆过滤器:
import java.util.BitSet;
public class BloomFilter<T> {
// 默认容量
private static final int DEFAULT_SIZE = 1 << 30;
// hash随机种子
private static final int[] SEEDS = {3, 13, 46, 71, 91, 134};
private BitSet bitSet;
// hash func
private HashFunc<T>[] func;
public BloomFilter() {
bitSet = new BitSet(DEFAULT_SIZE);
func = new HashFunc[SEEDS.length];
for (int i = 0; i < func.length; i++) {
func[i] = new HashFunc(SEEDS[i], DEFAULT_SIZE);
}
}
// 添加元素
public void add(T value) {
for (HashFunc<T> f : func) {
bitSet.set(f.hash(value), true);
}
}
// 判断元素是否在filter中
public boolean contains(T value) {
for (HashFunc<T> f : func) {
if (!bitSet.get(f.hash(value))) {
return false;
}
}
return true;
}
private static class HashFunc<T> {
private final int seed;
private final int capacity;
public HashFunc(int seed, int capacity) {
this.seed = seed;
this.capacity = capacity;
}
public int hash(T value) {
int h;
return (value == null) ? 0 : Math.abs(seed * (capacity - 1) & ((h = value.hashCode())) ^ (h >>> 16));
}
}
}