1.引言
1.1.背景:
通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,这个时候往往我们都是采用链表、树、散列表(又叫哈希表,Hash table)或者其他集合将数据保存起来,然后进行对比判断,但是如果元素很多的情况,我们如果采用这种方式就会非常浪费空间,同时检索效率也会降低。这个时候我们就需要 BloomFilter 来帮助我们了。
1.2.布隆过滤器:
布隆过滤器(英语:Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。主要用于判断一个元素是否在一个集合中。
2.算法描述
2.1.布隆过滤器数据结构:
BloomFilter 是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。
在初始状态时,对于长度为 m 的位数组,它的所有位都被置为0,当有变量被加入集合时,通过 K 个映射函数将这个变量映射成位图中的 K 个点,把它们置为 1,如下图所示:
查询某个变量的时候我们只要看看这些点是不是都是 1 就可以大概率知道集合中有没有它了
- 如果这些点有任何一个 0,则被查询变量一定不在;
- 如果都是 1,则被查询变量很可能存在
为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。
2.2.误判率:
布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置1。
这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。
3.优缺点
3.1.优点:
-
在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数 O ( K ) O(K) O(K),另外,散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势;
-
布隆过滤器可以表示全集,其它任何数据结构都不能;
-
保密性强。布隆过滤器不存储元素本身。
3.2.缺点:
- 有一定误算率,且删除元素困难。有限大小的位向量使得哈希映射必然存在碰撞, 向量中的一位可能被映射多次,所以布隆过滤器无法删除已 经存储的元素。
- 无法获取元素本身。由于布隆过滤器使用哈希映射对集合元素进行间接 存储表示,所以回答元素集合查询,而无法获得该元素的其 他信息。
- 对于高存储高准确性,无法发挥其最高性能。虽然布隆过滤器的空间开销小于哈希表,但一旦存储 集合过大或应用场景对查询的准确性过高,对应的布隆过滤 器就无法完全存在于内存而需迁移至二级存储中,这会大大 影响布隆过滤器的查询效率,而无法完全发挥出其最高性能。
- 对空间的利用并不高效。而一旦元素个数超出预期就会 导致其性能大幅下降,甚至出现无法继续使用的情况。
4.应用场景
布隆过滤器最大的用处就在于判断某样东西一定不存在或者可能存在,而这个就是查询元素的结果。
4.1.其查询元素的过程如下:
- 通过k个无偏hash函数计算得到k个hash值
- 依次取模数组长度,得到数组索引
- 判断索引处的值是否全部为1,如果全部为1则存在(这种存在可能是误判),如果存在一个0则必定不存在
4.2.布隆过滤器的典型应用有:
- 数据库防止穿库。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。
- 业务场景中判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。
- 在缓存穿透上经常有用到,我们可以在缓存之前再加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回。
- WEB拦截器,如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。Squid 网页代理缓存服务器在 cache digests 中就使用了布隆过滤器。Google Chrome浏览器使用了布隆过滤器加速安全浏览服务
- Venti 文档存储系统也采用布隆过滤器来检测先前存储的数据。
- SPIN 模型检测器也使用布隆过滤器在大规模验证问题时跟踪可达状态空间。
4.3.缓存穿透:
在Redis中布隆过滤器主要解决的是缓存穿透的问题。
4.3.1.什么是缓存穿透?
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
4.3.2解决方案:
(1) 基本方案:
首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
(2) 布隆过滤器:
把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
5.Java代码的实现
手写的一个简单的布隆过滤器
MyBoomFilter:
package com.feng.filter;
import java.util.BitSet;
public class MyBloomFilter {
/**
* 一个长度为10 亿的比特位
*/
private static final int DEFAULT_SIZE = 256 << 22;
/**
* 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
*/
private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};
/**
* 相当于构建 8 个不同的hash算法
*/
private static HashFunction[] functions = new HashFunction[seeds.length];
/**
* 初始化布隆过滤器的 bitmap
*/
private static BitSet bitset = new BitSet(DEFAULT_SIZE);
/**
* 添加数据
*
* @param value 需要加入的值
*/
public static void add(String value) {
if (value != null) {
for (HashFunction f : functions) {
//计算 hash 值并修改 bitmap 中相应位置为 true
bitset.set(f.hash(value), true);
}
}
}
/**
* 判断相应元素是否存在
* @param value 需要判断的元素
* @return 结果
*/
public static boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (HashFunction f : functions) {
ret = bitset.get(f.hash(value));
//一个 hash 函数返回 false 则跳出循环
if (!ret) {
break;
}
}
return ret;
}
/**
* 模拟用户是不是会员,或用户在不在线。。。
*/
public static void main(String[] args) {
for (int i = 0; i < seeds.length; i++) {
functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
}
// 添加1亿数据
for (int i = 0; i < 100000000; i++) {
add(String.valueOf(i));
}
String id = "123456789";
//再添加一下
add(id);
System.out.println(contains(id)); // true
System.out.println("" + contains("234567890")); //false
}
}
class HashFunction {
private int size;
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
this.seed = seed;
}
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);
}
int r = (size - 1) & result;
return (size - 1) & result;
}
}
调用谷歌的guava库:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
BloomFilterDemo:
package com.feng.demo;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterDemo {
public static void main(String[] args) {
//后边两个参数:预计包含的数据量,和允许的误差值
int errorCount = 0;
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100000, 0.01);
for (int i = 0; i < 100000; i++) {
bloomFilter.put(i);
}
System.out.println(bloomFilter.mightContain(1));
System.out.println(bloomFilter.mightContain(2));
System.out.println(bloomFilter.mightContain(3));
System.out.println(bloomFilter.mightContain(100000));
System.out.println(bloomFilter.mightContain(99999));
for (int i = 100000; i < 200000; i++) {
if (bloomFilter.mightContain(i)){
errorCount++;
}
}
System.out.println("判错次数: "+ errorCount);
}
}
/*
true
true
true
false
true
判错次数: 1018
*/
6.扩展
为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。论文《Cuckoo Filter:Better Than Bloom》作者将布谷鸟过滤器和布隆过滤器进行了深入的对比。相比布谷鸟过滤器而言布隆过滤器有以下不足:查询性能弱、空间利用效率低、不支持反向操作(删除)以及不支持计数。
7.总结和展望
随着数据规模的不断扩大,越来越多的应用场景在执行查询请求时需要设计海量的数据,且场景对于这些请求的性能要求也越来越高,空间紧凑,存在少量查询误判但效率极高的数据结构布隆过滤器是解决元素成员资格查询的一个常见工具。本文从为以后布隆过滤器可能的研究方向提供了参考。
8.参考文献
[1] 华文镝.Bloom Filter 研究综述[J/OL].计算机应用,2022(2)7.
9.项目中
实际项目中使用Redis布隆过滤器(bloom filter)我的裤衩呢的博客-CSDN博客redistemplate布隆过滤器
凑,存在少量查询误判但效率极高的数据结构布隆过滤器是解决元素成员资格查询的一个常见工具。本文从为以后布隆过滤器可能的研究方向提供了参考。
8.参考文献
[1] 华文镝.Bloom Filter 研究综述[J/OL].计算机应用,2022(2)7.
9.项目中
实际项目中使用Redis布隆过滤器(bloom filter)我的裤衩呢的博客-CSDN博客redistemplate布隆过滤器
END
还活着的!