布隆过滤器

布隆过滤器

数据结构

布隆过滤器它实际上是一个很长的二进制向量和一系列随机映射函数。以Redis中的布隆过滤器实现为例,Redis中的布隆过滤器底层是一个巨大型位数组(二进制数组)+多个无偏hash函数。
一个巨大型位数组(二进制数组):

e94e504adc5a75a2d7f562dc44166511

多个无偏hash函数:.
无偏hash函数就是能把元素的hash值计算的比较均匀的hash函数,能使得计算后的元素下标比较均匀的映射到位数组中。如下就是一个简单的布隆过滤器示意图,其中k1、k2代表增加的元素,a、b、c即为无偏hash函数,最下层则为二进制数组

9ebde5c11ad69447314c216acf188fc8

参数设置

错误率 f

  • f 越小则二进制数位数越长,空间占用大
  • f 越小则hash函数越多,计算耗时长

增加数据

将数据带入到每个hash函数分别计算出下标值,将布隆过滤器(巨大二进制数)对应下标的位置1

查询数据

一样通过hash函数计算出各下标,判断是否所有的位都为1,则数据存在!所以要求二进制数位比较大

Redis集成

  1. 下载RedisBloom

(1)下载插件压缩包

1
wget https://github.com/RedisLabsModules/rebloom/archive/v2.2.6.tar.gz

(2)解压

1
tar -zxvf v2.2.6.tar.gz

(3)编译插件

1
2
cd RedisBloom-2.2.6/
make

编译成功后看到redisbloom.so文件即可

原文链接:https://blog.csdn.net/qq_41125219/article/details/119982158

  1. Redis配置文件修改

在redis.conf配置文件中加入如RedisBloom的redisbloom.so文件的地址
如果是集群则每个配置文件中都需要加入redisbloom.so文件的地址
添加完成后需要重启redis

1
loadmodule /usr/local/soft/RedisBloom-2.2.6/redisbloom.so

redis.conf配置文件中预置了loadmodule的配置项,我们可以直接在这里修改,后续修改会更加方便。

f69019f455492424ccb0fb7082ceb677

保存退出后一定要记得重启Redis!
保存退出后一定要记得重启Redis!
保存退出后一定要记得重启Redis!

项目实践

image-20230715230026883

请求首先由bloom过滤器做筛选:

  1. 不存在直接返回异常
  2. 存在则继续向redis查询
  3. 若redis无,则在数据库查询并缓存数据到redis

实现方案:

  1. 将可能发生缓存穿透的数据在Spring项目初始化的时候加载并保存在bloom过滤器中

注意:这里使用**@EventListener(ApplicationReadyEvent.class)注解将RBloomFilter初始化,并注册为bean,方便在业务层注入调用contains**方法!

在这里为什么会选择使用**@EventListener(ApplicationReadyEvent.class)**注解初始化bean,而不是选择在配置类中注入bean是因为:这个注解下的方法可以在所有bean初始化后再执行,而在配置类注入shopService时会发生shopService并未初始化,而导致注入失败

Tip:当Spring Boot应用程序完成所有bean的初始化并准备好接收请求时,它会发布一个ApplicationReadyEvent事件。你可以通过创建一个带有**@EventListener**注解的方法来监听这个事件,并在这个方法中执行你的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component  
public class MyStartupRunner {

@Autowired
RedissonClient redisson;

@Autowired
IShopService shopService;


/** 预计插入的数据 */
private static final Integer expectedInsertions = 100;
/** 误判率 */
private static final Double fpp = 0.0001;

@EventListener(ApplicationReadyEvent.class)
@Bean
public RBloomFilter<Long> bloomFilter() {
//1.创建BloomFilter,并初始化
RBloomFilter<Long> bloomFilter = redisson.getBloomFilter("ShopIdFilter");
bloomFilter.tryInit(expectedInsertions, fpp);
//2.利用shopService从数据库获取shopId
//3.将数据插入BloomFilter
shopService.list().forEach(shop -> {
bloomFilter.add(shop.getId());
});
return bloomFilter;
}
}
  1. 使用bloom过滤器解决缓存穿透
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public Result queryShopById(Long id) {
if (bloomFilter.contains(id)){
//查询redis中是否有数据
String json = stringRedisTemplate.opsForValue().get(SHOP_DETAIL + id);
if (StrUtil.isNotEmpty(json)){
Shop shop = JSONUtil.toBean(json, Shop.class);
return Result.ok(shop);
}else {
//未命中访问数据库(并将数据保存在redis)
Shop shop = getById(id);
if (Objects.isNull(shop)) {
//不存在,则将空字符串保存在Redis
stringRedisTemplate.opsForValue().set(SHOP_DETAIL + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("不存在此商铺!(数据库回应)");
}else {
//存在,将数据返回保存在Redis
stringRedisTemplate.opsForValue().set(SHOP_DETAIL + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
}
}else {
return Result.fail("不存在此商铺!(bloom过滤)");
}

}