谈谈布隆过滤器(Bloom Filter)?
我们都知道布隆过滤器对于缓存穿透是一个很好的解决方案。
那什么是布隆过滤器呢?
布隆过滤器是一个用一定的误判率来换取空间效率的概率性数据结构。它本身是一个很长的二进制数组,只存储0和1,主要用于判断某个元素是否存在:0代表不存在,1代表存在。
布隆过滤器存入数据的过程
- 1.通过K个哈希函数计算,然后返回K个已经计算出的hash值;
- 2.这这K个hash值映射到对应的二进制数组的下标;
- 3.将这K个下标对应的二进制数据改成1。
布隆过滤器查询数据的过程
- 1.对这个数据进行多次hash运算;
- 2.查看运算结果对应下标位置的数据是否为1,如果都为1则说明不一定存在,如果有一个不为1,则说明一定不存在。
介绍完布隆过滤器的存入和查询流程,那么请问布隆过滤器有删除操作吗?
布隆过滤器没有删除操作。
由于不同的数据经过hash算法得到的结果是有可能相同的。因此如果我们想要删除数据A,就需要将其对应的几个bit位都由1改为0;但如果这几个bit位同时被数据B所占有的话,此时再去查询数据B时就会直接返回false,然而实际上我们并没有删除数据B。
那么如何解决删除问题呢?
计数删除。计数删除需要存储一个数值,而不是原先的 bit 位,所以会增大占用的内存大小。这样的话,增加一个值就是将对应索引槽上存储的值加一,删除则是减一,判断是否存在则是看值是否大于0。
为什么会存在误判率问题?
原因是不同的数据经过hash算法得到的结果是有可能相同的。因此布隆过滤器判断不存在的一定不存在,判断存在则可能存在,也可能不存在。
如何降低误判率?
- 加大数组的长度,从而降低哈希碰撞的概率;
- 增加哈希函数的个数,从而降低哈希碰撞的概率;
SpringBoot如何实现布隆过滤器?
google的guava包已经实现了核心算法,所以利用它来实现布隆过滤器。
- 引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
- 编写测试类
public class BloomFilterCase {
/**
* 预计要插入多少数据
*/
private static int size = 1000000;
/**
* 期望的误判率
*/
private static double fpp = 0.01;
/**
* 布隆过滤器
*/
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
public static void main(String[] args) {
// 插入10万样本数据
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
// 用另外十万测试数据,测试误判率
int count = 0;
for (int i = size; i < size + 100000; i++) {
if (bloomFilter.mightContain(i)) {
count++;
System.out.println(i + "误判了");
}
}
System.out.println("总共的误判数:" + count);
}
}
这里我们插,1 - 100W数据,预计误判率为0.01,我们从1000001-1100000测试,后10W条数据肯定是不存在1-1000000中的,我们看他的误判数。
误判率的原理是什么?
(1)将误判率改为0.01,以Debug模式进入布隆过滤器的create方法。
我们发现numBits为9585058 , numHashFunctions为7 ,
numBits就是这个布隆过滤器占用的内存空间,numHashFunctions表示插入数据时对这个数据进行几次hash运算。
(2)将误判率改为0.001,以Debug模式进入布隆过滤器的create方法。
我们发现numBits为 14377587, numHashFunctions为 10,也就是说我们误判率设置的越小,这个二进制向量占用的内存空间就越大,某个数据进行插入的时候经过的hash运算就更多,因为这样才能尽可能将数据保存在多个下标位置。
如何利用布隆过滤器来解决缓存穿透?
- 引入redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.4</version>
</dependency>
- 配置
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
- 布隆过滤器的封装类
@Component
public class BloomFilterUtils {
@Autowired
private RedissonClient redisson;
private RBloomFilter<Long> bloomFilter;
@PostConstruct
private void initBloomFilter(){
this.bloomFilter = redisson.getBloomFilter("Id_List");
//初始化布隆过滤器:预计元素为10000L,误差率为3%
this.bloomFilter.tryInit(10000L,0.03);
}
public boolean isContains(Long id){
return this.bloomFilter.contains(id);
}
public void put(Long id){
this.bloomFilter.add(id);
}
}
首先可以用isContains方法进行判断某个id存在与否,如果存在则直接返回,再进行查询数据库,如果数据库中不存在,则将这个id,放入过滤器。