赞
踩
什么是布隆过滤器?
布隆过滤器有什么作用?
微服务中布隆过滤器的方案?
布隆过滤器(Bloom Filter)是由 Bloom 于 1970 年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
位数组中的每个元素都只占有1 bit,并且每个元素只能是0或1。这样申请一个100万的数组只占用1000000 Bit /8= 125000 Byte = 122 kb的空间。
Bloom提出的这种用来检索元素是否在给定大集合中的数据架构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且理论下,添加到集合的元素越多,误报的可能性就越大。
1 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值
2.根据得到得哈希值,在位数组中把对应下标得位置为1
import java.util.BitSet;
public class MyBloomFilter {
/**
* 位数组的大小
*/
private static final int DEFAULT_SIZE = 2 << 24;
/**
* 通过这个数组可以创建 6 个不同的哈希函数
*/
private static final int[] SEEDS = new int[]{5, 12, 23, 71, 95, 113};
/**
* 位数组。数组中的元素只能是 0 或者 1
*/
private BitSet bits = new BitSet(DEFAULT_SIZE);
/**
* 存放包含 hash 函数的类的数组
*/
private SimpleHash[] func = new SimpleHash[SEEDS.length];
/**
* 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
*/
public MyBloomFilter() {
// 初始化多个不同的 Hash 函数
for (int i = 0; i < SEEDS.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
}
}
/**
* 添加元素到位数组
*/
public void add(Object value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
/**
* 判断指定元素是否存在于位数组
*/
public boolean contains(Object value) {
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
/**
* 静态内部类。用于 hash 操作!
*/
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
/**
* 计算 hash 值
*/
public int hash(Object value) {
int h;
return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
}
}
}
测试
@Test
public void testMyBloom(){
String value1 = "xiaowang";
String value2 = "xiaoliu";
String value3 = "xiao";
MyBloomFilter filter = new MyBloomFilter();
System.out.println(filter.contains(value1));
System.out.println(filter.contains(value2));
System.out.println(filter.contains(value3));
filter.add(value1);
filter.add(value2);
System.out.println(filter.contains(value1));
System.out.println(filter.contains(value2));
System.out.println(filter.contains(value3));
}
运行结果,可以看到布隆过滤器可以轻易的辨识指定key是否存在。
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
@Test
public void testBloom() {
/**
* expectedInsertions 预期插入值
* 这个值的设置相当重要,如果设置的过小很容易导致饱和而导致误报率急剧上升,如果设置的过大,也会对内存造成浪费,所以要根据实际情况来定
* fpp 误差率,例如:0.01,表示误差率为1%
*/
int expectedInsertions = 100_0000;//期望插入的数据量
double fpp = 0.01; //误判率
// 创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), expectedInsertions, fpp);
BloomFilter<Long> filter2 = BloomFilter.create(Funnels.longFunnel(), expectedInsertions, fpp);
BloomFilter<String> filter3 = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp);
System.out.println("------------未插入数据前检测-----------");
for(int i=899990;i<90_0003;i++){
// 判断指定元素是否存在
System.out.println(i+" 是否存在: "+filter.mightContain(i));
}
long s1=System.currentTimeMillis();
for (int i = 0; i < 90_0000; i++) {//向过滤器添加90万数据
// 将元素添加进布隆过滤器
filter.put(i);
}
System.out.println("添加90_0000条数据耗时:"+(System.currentTimeMillis()-s1)+" ms");
System.out.println("------------插入数据后检测-----------");
long s2=System.currentTimeMillis();
for(int i=899990;i<90_0003;i++){
// 判断指定元素是否存在
System.out.println(i+" 是否存在: "+filter.mightContain(i));
}
System.out.println("检测耗时:"+(System.currentTimeMillis()-s2)+" ms");
}
demo示例Bloom过滤器预期100万条记录(id),实际添加了90万条记录,设定的误差率率为1%
运行结果,可以看到它能准确的判断id是否存在。
在我们的示例中,当 mightContain() 方法返回 true 时,我们可以 99%确定该元素在过滤器中,当过滤器返回 false 时,我们可以 100%确定该元素不存在于过滤器中。
Long类型的id
BloomFilter<Long> filter2 = BloomFilter.create(Funnels.longFunnel(), expectedInsertions, fpp);
String类型的内容
BloomFilter<String> filter3 = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp);
使用Guava提供的布隆过滤器,需要每个服务上都有这份数据,这就意味着在微服务之间(同一微服务的多实例)无法实现共享,各自都需要建立私有数据在内存中。为了解决这个问题,我们就需要使用到redis中的布隆过滤器。
官方demo:https://github.com/dengliming/redis-modules-java
官方API:https://github.com/dengliming/redis-modules-java/tree/master/redisbloom
这里我们使用docker进行安装,因为操作起来比较简单。
docker run -d -p 16379:6379 --name redis-redisbloom redislabs/rebloom:latest
相关的命令请见
https://hub.docker.com/r/redislabs/rebloom/
<dependency>
<groupId>io.github.dengliming.redismodule</groupId>
<artifactId>redisbloom</artifactId>
<version>2.0.1</version>
</dependency>
<!-- release -->
<dependency>
<groupId>io.github.dengliming.redismodule</groupId>
<artifactId>all</artifactId>
<version>2.0.1</version>
</dependency>
这里直接使用的官方demo
package com.it2.springbootbloom;
import io.github.dengliming.redismodule.redisbloom.BloomFilter;
import io.github.dengliming.redismodule.redisbloom.client.RedisBloomClient;
import org.junit.jupiter.api.Test;
import org.redisson.config.Config;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class BloomFilterTest2 {
@Test
public void testBloom(){
Config config = new Config();
config.useSingleServer().setAddress("redis://106.13.2.249:16379");//可以选择单机,也可以选择集群
RedisBloomClient redisBloomClient = new RedisBloomClient(config);
BloomFilter bloomFilter = redisBloomClient.getRBloomFilter("bf");
bloomFilter.create(0.1d, 100);//预期插入值100,误差率(10%)
bloomFilter.madd(new String[] {"a", "b", "c"});//添加数据
System.out.println(bloomFilter.exists("a"));//a是否存在
System.out.println(bloomFilter.exists("d"));//d是否存在
redisBloomClient.shutdown();
}
}
运行代码测试
redisbloom 的容量是多少?
因为redisbloom 使用的是setbit来实现的布隆过滤器。官方文档介绍的容量是
2
32
2^{32}
232,即为512MB。
官方地址:https://redis.io/commands/setbit/
Sets or clears the bit at offset in the string value stored at key.
The bit is either set or cleared depending on value, which can be either 0 or 1.
When key does not exist, a new string value is created. The string is grown to make sure it can hold a bit at offset. The offset argument is required to be greater than or equal to 0, and smaller than 2^32 (this limits bitmaps to 512MB). When the string at key is grown, added bits are set to 0.
Warning: When setting the last possible bit (offset equal to 2^32 -1) and the string value stored at key does not yet hold a string value, or holds a small string value, Redis needs to allocate all intermediate memory which can block the server for some time. On a 2010 MacBook Pro, setting bit number 2^32 -1 (512MB allocation) takes ~300ms, setting bit number 2^30 -1 (128MB allocation) takes ~80ms, setting bit number 2^28 -1 (32MB allocation) takes ~30ms and setting bit number 2^26 -1 (8MB allocation) takes ~8ms. Note that once this first allocation is done, subsequent calls to SETBIT for the same key will not have the allocation overhead.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。