这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战
当接到一个集合内去重的需求时,我们第一个想到的就是HashSet
。确实,通过hashCode()
和equals()
方法,HashSet
可以实现高效快速严格地避免集合内的元素重复。
但是,当集合元素数量过大时,HashSet
所占内存就会变得非常大,这么大的HashSet
集合显然是不可取的。这时候就需要布隆过滤器了。
概念
布隆过滤器本质上是一种比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在” 。
数据结构
布隆过滤器由一个BitMap组成。所谓的BitMap,本质上就是一个数组。与寻常数组不同的是,BitMap一个数组元素占一个bit,这一特性决定了BitMap能够极大地节省空间。
当使用布隆过滤器时,每添加一个元素都会进行以下操作:
- 对需要添加的元素进行n种hash计算。
- 将hash计算的结果对应的BitMap位进行置1操作。
而对于查询操作来说(判断某元素是否存在),将会进行以下操作:
- 对需要查询的元素进行n种hash计算。
- 查询hash计算的结果对应的BitMap位是否为1。若都为1,则代表该元素有可能存在;若存在非1位,则代表该元素一定不存在。
误判
需要注意的是,布隆过滤器无法确定元素存在,只能确定元素不存在。出现的原因是多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。
这种情况也造成了布隆过滤器的删除问题,即布隆过滤器不存在删除操作。因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。
实现
自实现
通过上面的原理,我们可以自己实现一个布隆过滤器,示例如下:
package com.example.springtest;
import java.util.BitSet;
public class BloomFilter {
// 容量为1左移30位 1073741824
private static final int SIZE = 1 << 30;
// 布隆过滤器底层数据结构
private BitSet bitSet = new BitSet(SIZE);
// 自定义哈希函数的seed
private static final int[] seeds = {3, 37, 61};
// 自定义哈希函数集
private static HashFunction[] functions = new HashFunction[seeds.length];
public BloomFilter() {
for (int i = 0; i < functions.length; i++) {
HashFunction hashFunction = new HashFunction(SIZE, seeds[i]);
functions[i] = hashFunction;
}
}
/**
* @Description 往布隆过滤器添加元素
* @Param [s]
* @return void
**/
public void add(String s){
for (HashFunction function : functions) {
int hash = function.hash(s);
bitSet.set(hash,true);
}
}
/**
* @Description 判断元素是否存在
* @Param [s]
* @return boolean
**/
public boolean contains(String s){
int containsCount = 0;
for (HashFunction function : functions) {
int hash = function.hash(s);
boolean b = bitSet.get(hash);
if (b){
containsCount++;
}
}
// 判断是否每位都是1
// 若是则可能存在,若不是则一定不存在
if (containsCount >= functions.length){
return true;
}else {
return 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;
}
}
测试代码如下:
public class BloomDemo {
public static void main(String[] args) {
// 往布隆过滤器内添加100个元素
BloomFilter bloomFilter = new BloomFilter();
for (int i = 0; i < 100; i++) {
bloomFilter.add(String.valueOf(i));
}
// 验证
System.out.println("30:"+bloomFilter.contains(String.valueOf(30)));
System.out.println("71:"+bloomFilter.contains(String.valueOf(71)));
System.out.println("93:"+bloomFilter.contains(String.valueOf(93)));
System.out.println("3:"+bloomFilter.contains(String.valueOf(3)));
System.out.println("149:"+bloomFilter.contains(String.valueOf(149)));
System.out.println("179:"+bloomFilter.contains(String.valueOf(179)));
}
}
执行结果:
现有实现
其实对于布隆过滤器,已有现成的更好的实现。
以guava为例
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
以BloomFilter.create()
即可创建一个布隆过滤器
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100000, 0.01);
其中:
Funnel
是Guava中定义的一个接口,它和PrimitiveSink配套使用,主要是把任意类型的数据转化成HashCode (byte数组)。Guava预定义了一些原生类型的Funnel,如String、Long、Integer等等。
100000
是预计布隆过滤器元素的容量。
0.01
是允许的误差率,需小于1。
通过以上的参数,BloomFilter将会自行计算出适合的容量。
/**
* Computes m (total bits of Bloom filter) which is expected to achieve, for the specified
* expected insertions, the required false positive probability.
*
* <p>See http://en.wikipedia.org/wiki/Bloom_filter#Probability_of_false_positives for the
* formula.
*
* @param n expected insertions (must be positive)
* @param p false positive rate (must be 0 < p < 1)
*/
@VisibleForTesting
static long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
Redis实现
而在分布式环境中,使用Redis实现布隆过滤器更为适合。
安装
在Redis中,布隆过滤器以插件的形式提供,使用前需要安装。
-
下载安装包
wget https://github.com/RedisBloom/RedisBloom/archive/refs/tags/v2.2.6.tar.gz
-
解压并编译
tar -zxf v2.2.6.tar.gz cd RedisBloom-2.2.6/ make
-
在redis.conf中配置该模块
################################## MODULES ##################################### # Load modules at startup. If the server is not able to load modules # it will abort. It is possible to use multiple loadmodule directives. # # loadmodule /path/to/my_module.so # loadmodule /path/to/other_module.so loadmodule /usr/local/RedisBloom-2.2.6/redisbloom.so
-
启动redis,注意指定配置文件启动
redis-server
使用
RedisBloom的基础使用命令如下:
命令 | 格式 | 说明 |
---|---|---|
bf.reserve | bf.reserve {key} {error_rate} {initial_size} | 创建一个大小为initial_size位向量长度,错误率为error_rate的空的Bloom过滤器 |
bf.add | bf.add{key} {item} | 向key指定的Bloom中添加一个元素item |
bf.madd | bf.madd {key} {item} {item2} {item3} … | 一次添加多个元素 |
bf.exists | bf.exists {key} {item} | 查询元素是否存在 |
bf.mexists | bf.mexists {key} {item} {item2} {item3} … | 检查多个元素是否存在 |
bf.info | bf.info {key} | 查询key指定的Bloom的信息 |
bf.debug | bf.debug {key} | 查看BloomFilter的内部详细信息(如每层的元素个数、错误率等) |
cf.reserve | cf.reserve {key} {initial_size} | 创建一个initial_size位向量长度的空的Bloom过滤器 |
cf.add | cf.add {key} {item} | 向key指定的Bloom中添加一个元素item |
cf.exists | cf.exists {key} {item} | 检查该元素是否存在 |
bf.scandump | bf.scandump {key} {iter} | (key:布隆过滤器的名字,iter:首次调用传值0,或者上次调用此命令返回的结果值)对Bloom进行增量持久化操作(增量保存) |
bf.localchunk | bf.localchunk {key} {iter} {data} | 加载SCANDUMP持久化的Bloom数据(key:目标布隆过滤器的名字,iter:SCANDUMP返回的迭代器的值,和data一一对应,data:SCANDUMP返回的数据块(data chunk)) |
使用示例如下:
# 初始化一个布隆过滤器,每个参数分表代表 key 错误率 预计容量
127.0.0.1:6379> bf.reserve BFTest 0.01 10000
OK
# 添加元素
127.0.0.1:6379> bf.add BFTest 1
(integer) 1
127.0.0.1:6379> bf.add BFTest 2
(integer) 1
# 批量添加元素
127.0.0.1:6379> bf.madd BFTest 4 5 6 7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
# 判断元素是否存在
127.0.0.1:6379> BF.EXISTS BFTest 1
(integer) 1
127.0.0.1:6379> BF.EXISTS BFTest 2
(integer) 1
127.0.0.1:6379> BF.EXISTS BFTest 3
(integer) 0
应用场景
布隆过滤器的典型应用有:
- 防止数据库穿库。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。
- 防止缓存穿透。缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。使用布隆过滤器能够避免频繁查询不存在的数据,减轻数据库的压力。
- 业务场景中判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。
- WEB拦截器,如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。Squid 网页代理缓存服务器在 cache digests 中就使用了布隆过滤器。Google Chrome浏览器使用了布隆过滤器加速安全浏览服务