详解布隆过滤器(Bloom Filter)
背景
如果有一组结构化数据(通过记录ID标识),存储在一组数据文件中,如何确定哪个文件可能包含我们所需的数据呢?如果逐个读取文件,速度会极慢(尤其是要不断从磁盘将数据移入内存)。
一种解决方案是:构建一个索引表,其中每个数据文件对应一个索引,即将每个记录ID映射到数据文件中的偏移量(索引文件根据记录ID进行排序)。要搜索指定记录ID时,使用二分查找。但是,我们能做得更好吗?
算法描述
使用Bloom过滤器快速测试元素是否为集合的成员。返回结果为:可能在集合中,或绝对不在集合中。可能误报(即搜索一个不存在的元素,返回值为“存在”);不会出现漏报。随着过滤器中元素数量的增加,错误率也会增加。
一个空的Bloom过滤器是一个位数组,包含
m
位,全部设置为0。还有k
个不同的哈希函数:每个函数都将集合中的元素映射到m
位位置中的一个。
*
添加元素:将其输入到哈希函数中,得到k
个位位置,并将这些位置的位设置为1;
* 测试元素是否在集合中:将其输入到哈希函数中,得到k个位位置。 *
如果这些位置中的任何一个位是0,那么该元素肯定不在集合中;
* 如果全部是1,那么该元素可能在集合中。
为什么Bloom过滤器不能删除数据?
解答:无法判断待删除元素映射到的 k 位中,哪一个应该被清除。尽管将这 k 位中的任何一个设置为零就足以删除该元素,但它也会删除碰巧映射到该位的任何其他元素。因此清除任何位都会引入漏报的可能性。
然而,不允许删除的机制会导致其中的无效元素可能会越来越多,即实际已经在磁盘删除中的元素,但在Bloom过滤器中还认为可能存在,造成越来越多的false positive。
空间和时间优势
- 存储空间:常数
(O(k))
,因为其不需要存储实际数据项(适合有保密要求的场景); - 插入/查询时间:常数
(O(k))
, 不会随着元素增加而增加; - 空间复杂度为
O(m)
:不会随着元素增加而增加,占用空间少
缺点:
- 随着存入的元素数量增加,误报率随之增加;
- 一般情况下不能从布隆过滤器中删除元素。
误报率
FN(False Negative):集合里有某元素,查找结果是没有该元素(漏报)
- 恒有:FN=0
FP(False Positive):集合里没有某元素,查找结果是有该元素(误报)
其中:n为插入元素的数量;k为哈希次数;m为Bloom过滤器的长度 > 极端情况下,当布隆过滤器没有空闲空间时(满),每一次查询都会返回 true 。这也就意味着 m 的选择取决于期望预计添加元素的数量 n ,并且 m 需要远远大于 n 。
哈希函数的最佳数量
对于给定的 m
和 n
,使假阳性概率最小的
k
值为:
有:
即对于给定的假阳性概率 ε
,布隆过滤器 m
的长度与被过滤的元素数量n
成正比,并且所需的哈希函数数量仅取决于目标假阳性概率
ε
。
因此:
应用场景
去重统计
转载:https://blog.csdn.net/CrankZ/article/details/84928562
网页爬虫对 URL 去重,避免爬取相同的 URL 地址,有以下几种思路: 1. 用HashSet保存访问过的URL,只需接近O(1)的代价就可以查到一个URL是否被访问过了; * 太消耗内存。随着URL的增多,占用的内存会越来越多。就算只有1亿个URL,每个URL只算50个字符,就需要5GB内存。 2. URL经过MD5或SHA-1等单向哈希后再保存到HashSet或数据库; * 于字符串经过MD5处理后的信息摘要长度只有128Bit,SHA-1处理后也只有160Bit,因此方法3比方法2节省了好几倍的内存。 3. Bit-Map方法。建立一个BitSet,将每个URL经过一个哈希函数映射到某一位。 * 单一哈希函数发生冲突的概率太高。若要降低冲突发生的概率到1%,就要将BitSet的长度设置为URL个数的100倍。 4. 使用Bloom过滤器。
缓存穿透
缓存穿透:查询⼀个在缓存和数据库都不存在的数据,这个数据始终⽆法被缓存,导致每次请求都直接访问数据库,增加数据库的负载。典型的情况是攻击者可能通过构造不存在的 key ⼤量访问缓存,导致对数据库的频繁查询。
解决方案:利用Bloom过滤器我们可以预先把数据查询的主键,比如用户 ID 或文章 ID 缓存到过滤器中。要查询时:首先去Bloom过滤器查询该key是否存在;若存在,则进行下一步处理;若不存在,直接返回,因此不会触发后续的数据库查询。
实现:Google Guava
导入Guava库
1
2
3
4
5<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>新建一个 BloomFilterDemo 类,在 main 方法中我们通过 BloomFilter.create 方法来创建一个布隆过滤器,初始化 1 百万条数据到过滤器中;在原有的基础上增加 10000 条数据,并判断这些数据是否存在布隆过滤器中:
当以上代码运行后,控制台输出结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterDemo {
public static void main(String[] args) {
int total = 1000000; // 总数量
BloomFilter<CharSequence> bf =
BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total);
// 初始化 1000000 条数据到过滤器中
for (int i = 0; i < total; i++) {
bf.put("" + i);
}
// 判断值是否存在过滤器中
int count = 0;
for (int i = 0; i < total + 10000; i++) {
if (bf.mightContain("" + i)) {
count++;
}
}
System.out.println("已匹配数量 " + count);
}
}误判率为:309/(1000000 + 10000) * 100 ≈ 0.0305940594059405931
已匹配数量 1000309
提高匹配精度:在创建布隆过滤器的时候设置误判率 fpp:
> 在 BloomFilter 内部,误判率 fpp 的默认值是 0.03: >1
2
3BloomFilter<CharSequence> bf = BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8), total, 0.0002
);1
2
3
4// com/google/common/hash/BloomFilter.class
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long > > expectedInsertions) {
return create(funnel, expectedInsertions, 0.03D);
>}
误判率 fpp 的值越小,匹配的精度越高;当减少误判率 fpp 的值,需要的存储空间也越大。在实际使用过程中,需要在误判率和存储空间之间权衡。