布隆过滤器想必大家都听过,背过Redis面试题的兄弟应该都知道,布隆过滤器是解决缓存穿透问题的一种方法。但可能很少用过
布隆过滤器主要是为了解决海量数据的存在性问题。对于海量数据中判定某个数据是否存在且容忍轻微误差这一场景(比如缓存穿透、海量数据去重)来说,非常适合。
一、简介
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除
布隆过滤:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中。
假设布隆过滤器判断这个数据不存在,则直接返回
简单来说,布隆过滤器就是一个集合,用来存放数据,同时用以判断数据是否存在于这个集合中。这种方式优点在于节约内存空间,但存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突。
正常来说,数据量越大,出现误判的可能性就越大。
二、使用场景
-
判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,上亿)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤(判断一个邮件地址是否在垃圾邮件列表中)、黑名单功能(判断一个 IP 地址或手机号码是否在黑名单中)等等。
-
去重:比如爬给定网址的时候对已经爬取过的 URL 去重、对巨量的 QQ 号/订单号去重。
去重场景也需要用到判断给定数据是否存在,因此布隆过滤器主要是为了解决海量数据的存在性问题。
下面我们来简单使用一下
三、简单使用
1、导入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
2、代码测试
// 创建布隆过滤器对象:
// 参数1 = 预计插入的元素数量, 参数2 = 容错率(期望的误判率)
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
1500,
0.01);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));//false
System.out.println(filter.mightContain(2));//false
// 将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));//true
System.out.println(filter.mightContain(2));//true
四、项目实战
下面我们用布隆过滤器来解决缓存穿透
虽然缓存穿透问题可以用布隆过滤器来解决,但是布隆过滤器中的数据不易被删除,如果数据库中的数据有新增或者删除,布隆过滤器无法及时删除或新增数据。
上面所介绍的布隆过滤器其实有个问题
它最大的一个问题是它只能在同一个方法里面进行判断,不同的线程所创建的布隆过滤器会不一样。
比如说你在一个方法里面创建好了一个布隆过滤器,也添加了一些元素;但如果你在另一个方法里面去判断是查询不到的,也就类似于本地和分布式的区别。
所以我们这里采用Redis布隆过滤器。
Redis布隆过滤器需要在Redis上集成一个插件,选择对应Redis版本的插件,我这里使用的是Redis6,具体下载流程看:下载流程:硬核 | Redis 布隆(Bloom Filter)过滤器原理与实战-腾讯云开发者社区-腾讯云 (tencent.com)
1、缓存穿透思路
在数据到达数据库时,应该先判断布隆过滤器中是否存在key值,但是有个问题
布隆过滤器中原本不存在数据,那么又如何去判断数据是否存在?
我们需要先将数据添加到过滤器中,然后再让其发挥作用
布隆过滤器中数据Key的对象应该对应数据库,而不是缓存。
代码逻辑:
-
首先判断请求值key是否存在于布隆过滤器中
-
不存在,直接返回
-
存在,查询缓存中是否有对应数据
-
有,直接返回数据
-
没有,查询数据库
-
不存在,返回空数据
-
存在,更新缓存,返回数据
-
-
key过了布隆过滤器表示数据库中存在id对应的数据,剩下两种:
-
缓存key不存在—查询数据库
-
缓存key存在
-
key为空值
-
key中数据存在,直接返回
-
所以我们这里需要过滤掉key中数据为空的情况,保证放回的是真实存在的数据.
2、代码实现
1)导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.3</version>
</dependency>
2)配置Redisson
注意:如果项目中用到了Redis,数据库要和它的区别开,不然运行会发生冲突的
# 分布式限流Redisson 配置
redisson:
database: 1
host:
port: 6379
timeout: 5000
password:
3)Redisson配置类
package com.example.config;
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 分布式限流
*/
@Configuration
@ConfigurationProperties(prefix = "spring.redisson")
@Data
public class RedissonConfig {
private String host;
private Integer database;
private Integer port;
private String password;
//spring启动时,自动创建RedissonClient对象
@Bean
public RedissonClient getRedissonClient() {
Config config = new Config();
// 设置使用单个服务器
config.useSingleServer()
// 设置数据库
.setDatabase(database)
// 设置地址
.setAddress("redis://" + host + ":" + port)
// 设置密码
.setPassword(password);
// 创建Redisson客户端
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
2)布隆过滤管理类
创建布隆过滤器管理类,设置插入的元素数量和容错率
一般来说,使用管理类是为了降低代码的耦合性,可以让代码看上去更加美观,当然最重要的是实用。管理类不依赖于项目中的逻辑,在任何项目中都可以写入复用。
@Service
public class BloomFilterManager {
@Resource
private RedissonClient redissonClient;
/**
* 创建布隆过滤器
*
* @param filterName 过滤器名称
* @param expectedInsertions 预测插入数量
* @param falsePositiveRate 误判率
*/
public <T> RBloomFilter<T> create(String filterName, long expectedInsertions, double falsePositiveRate) {
RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(filterName);
bloomFilter.tryInit(expectedInsertions, falsePositiveRate);
return bloomFilter;
}
}
3)查询数据ID
@Select("SELECT id FROM tb_shop")
List<Long> selectAllIds();
在shopMapper中添加这段代码,查询数据库中所有数据的ID
4)布隆过滤器初始化
这步不知道你们能不能想到,有没有觉得很熟悉,嗯?
对了,上面一篇文章我讲过的,初始化注解,代码都还没删呢
项目启动时,初始化创建布隆过滤器,并把查询到的ID都插入过滤器中
通过Redis布隆过滤器,就相当于在Redis中创造了一个特殊容器来存储数据
这段代码的逻辑就是定义一个布隆过滤器,然后把查询到的shopid都写入到Redis布隆过滤器中
然后就可以在实际方法里面去查询指定的shopId元素是否真的存在了
@PostConstruct
public void init() {
//myMessageProducer.sendMessage("hmdp_exchange", "hmdp_routingKey", String.valueOf(1L));
// 启动项目时初始化bloomFilter
bloomFilter = bloomFilterManager.create(BLOOM_FILTER_SHOP, expectedInsertions, falseProbability);
// 设置缓存时间
int timeToLive = new Random().nextInt(200) + 300;
// 设置bloomFilter的过期时间
redissonClient.getBucket(BLOOM_FILTER_SHOP).set(null, timeToLive, TimeUnit.SECONDS);
List<Long> list = shopMapper.selectAllIds();
for (Long shopId : list) {
bloomFilter.add(shopId);
}
long elementCount = bloomFilter.count();
log.info("布隆过滤器加入元素个数为:{}.", elementCount);
}
初始化后,启动项目后,控制台也会自动执行查询方法,初始化成功
方法一: 启动项目时初始化bloomFilter,将数据库中查询的ID都添加到过滤器中
方法二:
-
首先判断请求值key是否存在于布隆过滤器中
-
不存在,直接返回
-
存在,查询缓存中是否有对应的数据
-
有,直接返回数据
-
没有,查询数据库
-
不存在,返回空
-
存在,更新缓存,返回数据
-
-
接下来我们需要来模拟情况:
1、数据库中存在,缓存中不存在
预期:能通过布隆,最后到达数据库,并将查询到的数据存入Redis
2、数据库和缓存中数据均存在
预期:通过布隆,到达缓存直接返回
3、布隆中key不存在
预期:直接失败
4、布隆中key存在,缓存中key存在但为空,数据库中数据不存在
最后来看结果:
今天的讲解就到这里,期待下期再见!希望的话,欢迎点赞关注!