布隆过滤器(Bloom Filter)是 1970 年由布隆提出的,是一种非常节省空间的概率数据结构,运行速度快,占用内存小,但是有一定的误判率且无法删除元素。它实际上是一个很长的二进制向量和一系列随机映射函数组成,主要用于判断一个元素是否在一个集合中。
通常我们都会遇到判断一个元素是否在某个集合中的业务场景,这个时候我们可能都是采用 HashMap的Put方法或者其他集合将数据保存起来,然后进行比较确定,但是如果元素很多的情况下,采用这种方式就会非常浪费空间,最终达到瓶颈,检索速度也会越来越慢,这时布隆过滤器(Bloom Filter)就应运而生了。
布隆过滤器中一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在。因此,布隆过滤器不适合那些对结果必须精准的应用场景。
如果允许误判率的话,可以使用布隆过滤器,只有你想不到的,没有你做不到的。
布隆过滤器是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。
对于长度为 m 的位数组,在初始状态时,它所有位置都被置为0,如下图所示:
位数组中的每个元素都只占用 1 bit ,并且数组中元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。
当一个元素加入布隆过滤器中的时候,会进行如下操作:
接着再添加一个值 “xinlang”,哈希函数的值是3、5、8,如下图所示:
这里需要注意的是,5 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此被覆盖了。
如下图所示:
结果得到三个 1 ,说明 “cunzai” 是有可能存在的。
为什么说是可能存在,而不是一定存在呢?主要分为以下几种情况:
因为映射函数本身就是散列函数,散列函数是会有碰撞的情况发生。
鉴于上面的情况,不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整哈希函数。
布隆过滤器判定某个元素存在,小概率会误判;布隆过滤器判定某个元素不在,则这个元素一定不在。
布隆过滤器对元素的删除,肯定不可以,会出现问题,比如上面添加元素的 bit 位 5 被两个变量的哈希值共同覆盖的情况下,一旦我们删除其中一个值。例如“xinlang”而将其置位 0,那么下次判断另一个值例如“baidu”是否存在的话,会直接返回 false,而实际上我们并没有删除它,这就导致了误判的问题。
首先引入Guava的依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
那么,在数据量很大的情况下,效率如何呢?
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 5000000);
for (int i = 0; i < 5000000; i++) {
bloomFilter.put(i);
}
long start = System.nanoTime();
if (bloomFilter.mightContain(500000)) {
System.out.println("成功过滤到500000");
}
long end = System.nanoTime();
System.out.println("布隆过滤器消耗时间"+(end - start)/1000000L+"毫秒");
成功过滤到500000
布隆过滤器消耗时间0毫秒
布隆过滤器消耗时间:0毫秒,有点不敢相信呢,匹配速度是不是很快?
那么,在数据量很大的情况下,1%的误判率结果如何?
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),5000000,0.01);
List<String> list = new ArrayList<>(5000000);
for (int i = 0; i < 5000000; i++) {
String uuid = UUID.randomUUID().toString();
bloomFilter.put(uuid);
list.add(uuid);
}
int mightContainNumber1= 0;
NumberFormat percentFormat =NumberFormat.getPercentInstance();
percentFormat.setMaximumFractionDigits(2);
for (int i=0;i < 500;i++){
String key = list.get(i);
if (bloomFilter.mightContain(key)){
mightContainNumber1++;
}
}
System.out.println("【key真实存在的情况】布隆过滤器认为存在的key值数:" + mightContainNumber1);
System.out.println("================================================================================");
int mightContainNumber2 = 0;
for (int i=0;i < 5000000;i++){
String key = UUID.randomUUID().toString();
if (bloomFilter.mightContain(key)){
mightContainNumber2++;
}
}
System.out.println("【key不存在的情况】布隆过滤器认为存在的key值数:" + mightContainNumber2);
System.out.println("【key不存在的情况】布隆过滤器的误判率为:" + percentFormat.format((float)mightContainNumber2 / 5000000));
【key真实存在的情况】布隆过滤器认为存在的key值数:500
================================================================================
【key不存在的情况】布隆过滤器认为存在的key值数:50389
【key不存在的情况】布隆过滤器的误判率为:1.01%
3%的误判率结果如何?
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),5000000,);
List<String> list = new ArrayList<>(5000000);
for (int i = 0; i < 5000000; i++) {
String uuid = UUID.randomUUID().toString();
bloomFilter.put(uuid);
list.add(uuid);
}
int mightContainNumber1= 0;
NumberFormat percentFormat =NumberFormat.getPercentInstance();
percentFormat.setMaximumFractionDigits(2);
for (int i=0;i < 500;i++){
String key = list.get(i);
if (bloomFilter.mightContain(key)){
mightContainNumber1++;
}
}
System.out.println("【key真实存在的情况】布隆过滤器认为存在的key值数:" + mightContainNumber1);
System.out.println("================================================================================");
int mightContainNumber2 = 0;
for (int i=0;i < 5000000;i++){
String key = UUID.randomUUID().toString();
if (bloomFilter.mightContain(key)){
mightContainNumber2++;
}
}
System.out.println("【key不存在的情况】布隆过滤器认为存在的key值数:" + mightContainNumber2);
System.out.println("【key不存在的情况】布隆过滤器的误判率为:" + percentFormat.format((float)mightContainNumber2 / 5000000));
创建一个最多添加 500 个整数的布隆过滤器,并且可以容忍误判率为百分之一(0.01)
public void bool(){
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 500, 0.01);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
}
【key真实存在的情况】布隆过滤器认为存在的key值数:500
================================================================================
【key不存在的情况】布隆过滤器认为存在的key值数:150591
【key不存在的情况】布隆过滤器的误判率为:3.01%
从上面的结果可以看出:
看源码可知这个3%的fpp是Guava中默认的fpp
public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}
如下地址是一个免费的在线布隆过滤器在线计算的网址:
点击这里
经过哈希计算次数设置为3次,这个3%的误判率和3次哈希运算需要多大空间位数组呢?
计算得到的结果是984.14KiB,100W的key才占用了0.98M,而如果是10亿呢,计算的结果是960M,这个内存空间是完全可以接受的。
Guava 提供的布隆过滤器的实现还是很不错的,但是它有一个重大的缺陷就是只能单机使用(另外,容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题就需要用到Redis中的布隆过滤器了。
1、点击https://redis.io/modules 找到RedisBloom
2、点击进去下载RedisBloom-master.zip文件,上传到linux
3、解压缩刚才的RedisBloom文件
unzip RedisBloom-master.zip
cd RedisBloom-master
编译安装
make
make完生成redisbloom.so,拷贝到redis的安装目录。
cp redisbloom.so /home/www/server/redis
在redis.conf配置文件中加入如RedisBloom的redisbloom.so文件的地址,如果是集群则每个配置文件中都需要加入redisbloom.so文件的地址
loadmodule /home/www/server/redis/redisbloom.so
保存以后重启redis服务
redis-server redis.conf --loadmodule /home/www/server/redis/redisbloom.so
上面我们有提到需要重启Redis,在本地和测试环境还可以,但是正式环境能不重启就不需要重启,那这么做可以不重启Redis,使用module load命令执行。
> MODULE LOAD /home/www/server/redis/redisbloom.so
> module list
1) 1) "name"
2) "bf"
3) "ver"
4) (integer) 999999
看到以上数据则说明redisbloom加载成功了,模块名name为"bf",模块版本号ver为999999。
使用布隆过滤器完整指令请到官网查看: 点击这里
#创建一个容量为5且不允许扩容的过滤器;
127.0.0.1:6379> bf.reserve name 0.1 5 NONSCALING
OK
127.0.0.1:6379> bf.madd name 1 2 3 4 5
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
#添加第6个元素时即提示布隆过滤器已满;
127.0.0.1:6379> bf.madd name 6
1) (error) ERR non scaling filter is full
127.0.0.1:6379> bf.info name
1) Capacity
2) (integer) 5
3) Size
4) (integer) 155
5) Number of filters
6) (integer) 1
7) Number of items inserted
8) (integer) 5
9) Expansion rate
10) (integer) 2
127.0.0.1:6379>
127.0.0.1:6379> bf.add name zhangsan1
(integer) 1
127.0.0.1:6379> bf.add name zhangsan2
(integer) 1
127.0.0.1:6379>
127.0.0.1:6379> bf.madd name zhangsan2 zhangsan3 zhangsan4 zhangsan5
1) (integer) 0
2) (integer) 1
3) (integer) 1
4) (integer) 1
127.0.0.1:6379> bf.exists name zhangsan2
(integer) 1
127.0.0.1:6379> bf.exists name zhangsan3
(integer) 1
127.0.0.1:6379> bf.mexists name zhangsan3 zhangsan4 zhangsan5
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379>
127.0.0.1:6379> bf.insert name items zhangsan1 zhangsan2 zhangsan3
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.insert name items zhangsan1 zhangsan2 zhangsan3
1) (integer) 0
2) (integer) 0
3) (integer) 0
127.0.0.1:6379> bf.insert name capacity 10000 error 0.00001 nocreate items zhangsan1 zhangsan2 zhangsan3
1) (integer) 0
2) (integer) 0
3) (integer) 0
127.0.0.1:6379>
127.0.0.1:6379> bf.insert name capacity 10000 error 0.00001 nocreate items zhangsan4 zhangsan5 zhangsan6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379>
127.0.0.1:6378> bf.madd name zhangsan1 zhangsan2 zhangsan3 zhangsan4 zhangsan5
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6378> bf.scandump name 0
1) (integer) 1
2) "\a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x02\x00\x00\x00\x8a\x00\x00\x00\x00\x00\x00\x00P\x04\x00\x00\x00\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00{\x14\xaeG\xe1zt?\xe9\x86/\xb25\x0e&@\b\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x00"
127.0.0.1:6378> bf.scandump name 1
1) (integer) 139
2) "\x80\x00\b\n\x00$\x00 \b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00\x00\x82$\x04\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x04\x01@\xa0\x00@\x00\x00\x00\x00\x00\x10@\x00\x02\"\x00 \x00\x00\x04\x00\x00\x00\x00\x00 \x00\x80\x00\x00\"\x04\x04\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00 \x80$\x00 \x00\x00 \x0c$\x00\x00\x00\b`\x00\x00\x00\x00\x00\x00\x00\x00\b\x80\x02 \x04\x00\x00\x00\x00\x00"
127.0.0.1:6378> bf.scandump name 200
1) (integer) 0
2) ""
127.0.0.1:6379> bf.info name
1) Capacity
2) (integer) 5
3) Size
4) (integer) 155
5) Number of filters
6) (integer) 1
7) Number of items inserted
8) (integer) 5
9) Expansion rate
#创建一个容量为5的布隆过滤器,其key为“name”;
127.0.0.1:6379> bf.reserve name0.1 5
OK
# 查看布隆过滤器的内部信息,此时布隆过滤器的层数为1
127.0.0.1:6379> bf.debug name
1) "size:0"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:0 ratio:0.05"
127.0.0.1:6379> bf.madd name 1 2 3 4 5
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
127.0.0.1:6379> bf.debug name
1) "size:5"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
127.0.0.1:6379> bf.madd name 11 12 13 14 15
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0
5) (integer) 1
# 添加10个元素后,此时布隆过滤器的层数变为2;
127.0.0.1:6379> bf.debug name
1) "size:9"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:4 ratio:0.025"
127.0.0.1:6379> bf.madd name 21 22 23
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.debug name
1) "size:12"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:7 ratio:0.025"
127.0.0.1:6379> bf.madd name 24 25
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> bf.debug name
1) "size:14"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:9 ratio:0.025"
127.0.0.1:6379> bf.madd name 31 32 33 34 35
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 1
5) (integer) 1
#添加20个元素后,此时布隆过滤器的层数变为3;
127.0.0.1:6379> bf.debug name
1) "size:19"
2) "bytes:4 bits:32 hashes:5 hashwidth:64 capacity:5 size:5 ratio:0.05"
3) "bytes:10 bits:80 hashes:6 hashwidth:64 capacity:10 size:10 ratio:0.025"
4) "bytes:23 bits:184 hashes:7 hashwidth:64 capacity:20 size:4 ratio:0.0125"
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.1</version>
</dependency>
public void patchingConsum(ConsumPatchingVO vo) throws ParseException {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://127.0.0.1:6379");
singleServerConfig.setPassword("123456");
RedissonClient redissonClient = Redisson.create(config);
RBloomFilter<String> bloom = redissonClient.getBloomFilter("name");
// 初始化布隆过滤器; 大小:100000,误判率:0.01
bloom.tryInit(100000L, 0.01);
// 新增10万条数据
for(int i=0;i<100000;i++) {
bloom.add("name" + i);
}
// 判断不存在于布隆过滤器中的元素
List<String> notExistList = new ArrayList<>();
for(int i=0;i<100000;i++) {
String str = "name" + i;
boolean notExist = bloom.contains(str);
if (notExist) {
notExistList.add(str);
}
}
if ($.isNotEmpty(notExistList) && notExistList.size() > 0 ) {
System.out.println("误判次数:"+notExistList.size());
}
}