什么是布隆过滤器
布隆过滤器能够实现使用较少的空间来判断一个指定的元素是否包含在一个集合中
布隆过滤器并不保存这些数据,所以只能判断是否存在,而并不能取出该元素
使用情景:凡是判断一个元素是否在一个集合中的操作,都可以使用它
布隆过滤器常见使用场景
idea中编写代码,一个单词是否包含在正确拼写的词库中(拼写不正确划绿线的提示)
公安系统,根据身份证号\人脸信息,判断该人是否在追逃名单中
爬虫检查一个网址是否被爬取过
......
为什么使用布隆过滤器
常规的检查一个元素是否在一个集合中的思路是遍历集合,判断元素是否相等
这样的查询效率非常低下
要保证快速确定一个元素是否在一个集合中,我们可以使用HashMap
因为HashMap内部的散列机制,保证更快更高效的找到元素
所以当数据量较小时,用HashMap或HashSet保存对象然后使用它来判定元素是否存在是不错的选择
但是如果数据量太大,每个元素都要生成哈希值来保存,我们也要依靠哈希值来判定是否存在,一般情况下,我们为了保证尽量少的哈希值冲突需要8字节哈希值做保存
long取值范围:-9223372036854775808-----9223372036854775807
5亿条数据 每条8字节计算后结果为需要3.72G内存,随着数据数量增长,占用内存数字可能更大
所以Hash散列或类似算法可以保证高效判断元素是否存在,但是消耗内存较多
所以我们使用布隆过滤器实现,高效判断是否存在的同时,还能节省内存的效果
但是布隆过滤器的算法天生会有误判情况,需要能够容忍,才能使用
布隆过滤器原理
巴顿.布隆于⼀九七零年提出
⼀个很长的⼆进制向量(位数组)
⼀系列随机函数 (哈希)
空间效率和查询效率⾼(又小又快)
有⼀定的误判率(哈希表是精确匹配)

如果我们向布隆过滤器中保存一个单词
semlinker
我们使用3个hash算法,找到布隆过滤器的位置
算法1:semlinker--> 2
算法2:semlinker--> 4
算法3:semlinker--> 6
会在布隆过滤器中产生如下影响

假设要查询 "Good" 这个单词在不在布隆过滤器中
算法1:Good-->7
算法2:Good-->3
算法3:Good-->6
我们判断Good单词生成的3,6,7三个位置,只要有一个位置是0
就表示当前集合中没有Good这个单词
一个布隆过滤器中不可能只存一个单词,一般布隆过滤器都是保存大量数据的
如果有新的元素保存在布隆过滤器中
kakuqo
算法1:kakuqo-->3
算法2:kakuqo-->4
算法3:kakuqo-->7

新的单词生成3,4,7三个位置
那么现在这个布隆过滤器中2,3,4,6,7都是1了
假如现在有单词bad,判断是否在布隆过滤器中
算法1:bad-->2
算法2:bad-->3
算法3:bad-->6
判断布隆过滤器2,3,6都是1,所以布隆过滤器会认为bad是存在于这个集合中的
误判就是这样产生的
布隆过滤器误判的效果:
布隆过滤器判断不存在的,一定不在集合中
布隆过滤器判断存在的,有可能不在集合中
过短的布隆过滤器如果保存了很多的数据,可能造成二进制位置值都是1的情况,一旦发送这种情况,布隆过滤器就会判断任何元素都在当前集合中,布隆过滤器也就失效了
所以我们要给布隆过滤器一个合适的大小才能让它更好的为程序服务
优点
空间效率和查询效率⾼
缺点
有⼀定误判率即可(可以控制在可接受范围内)。
删除元素困难(不能将该元素hash算法结果位置修改为0,因为可能会影响其他元素)
极端情况下,如果布隆过滤器所有位置都是1,那么任何元素都会被判断为存在于集合中
设计布隆过滤器
我们在启动布隆过滤器时,需要给它分配一个合理大小的内存
这个大小应该满足
1.内存占用在一个可接受范围
2.不能有太高的误判率(<1%)
内存约节省,误判率越高
内存越大,误判率越低
数学家已经给我们了公式计算误判率

上面是根据误判率计算布隆过滤器长度的公式
n 是已经添加元素的数量;
k 哈希的次数;
m 布隆过滤器的长度(位数的大小)
Pfp计算结果就是误判率
如果我们已经确定可接受的误判率,想计算需要多少位数布隆过滤器的长度
布隆过滤器计算器
windows安装redisbloom布隆过滤器
https://blog.youkuaiyun.com/weixin_44770915/article/details/107918770
布隆过滤器的测试
因为上面我们启用了虚拟机
我们在虚拟机中安装的redis是一个特殊版本的Redis
这个版本内置了操作布隆过滤器的lua脚本,支持布隆过滤的方法
我们可以直接使用,实现布隆过滤器
csmall-stock-webapi的pom文件 添加依赖
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在dev-yml文件中添加redis的配置
spring:
redis:
host: 192.168.137.150
port: 6379
password:
操作布隆过滤器有一个专门的类
实现对布隆过滤器的新增元素,检查元素等方法的实现
RedisBloomUtils类复制到需要使用布隆过滤器的项目中
@Component
public class RedisBloomUtils {
@Autowired
private StringRedisTemplate redisTemplate;
private static RedisScript<Boolean> bfreserveScript = new DefaultRedisScript<>("return redis.call('bf.reserve', KEYS[1], ARGV[1], ARGV[2])", Boolean.class);
private static RedisScript<Boolean> bfaddScript = new DefaultRedisScript<>("return redis.call('bf.add', KEYS[1], ARGV[1])", Boolean.class);
private static RedisScript<Boolean> bfexistsScript = new DefaultRedisScript<>("return redis.call('bf.exists', KEYS[1], ARGV[1])", Boolean.class);
private static String bfmaddScript = "return redis.call('bf.madd', KEYS[1], %s)";
private static String bfmexistsScript = "return redis.call('bf.mexists', KEYS[1], %s)";
public Boolean hasBloomFilter(String key){
return redisTemplate.hasKey(key);
}
/**
* 设置错误率和大小(需要在添加元素前调用,若已存在元素,则会报错)
* 错误率越低,需要的空间越大
* @param key
* @param errorRate 错误率,默认0.01
* @param initialSize 默认100,预计放入的元素数量,当实际数量超出这个数值时,误判率会上升,尽量估计一个准确数值再加上一定的冗余空间
* @return
*/
public Boolean bfreserve(String key, double errorRate, int initialSize){
return redisTemplate.execute(bfreserveScript, Arrays.asList(key), String.valueOf(errorRate), String.valueOf(initialSize));
}
/**
* 添加元素
* @param key
* @param value
* @return true表示添加成功,false表示添加失败(存在时会返回false)
*/
public Boolean bfadd(String key, String value){
return redisTemplate.execute(bfaddScript, Arrays.asList(key), value);
}
/**
* 查看元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)
* @param key
* @param value
* @return true表示存在,false表示不存在
*/
public Boolean bfexists(String key, String value){
return redisTemplate.execute(bfexistsScript, Arrays.asList(key), value);
}
/**
* 批量添加元素
* @param key
* @param values
* @return 按序 1表示添加成功,0表示添加失败
*/
public List<Integer> bfmadd(String key, String... values){
return (List<Integer>)redisTemplate.execute(this.generateScript(bfmaddScript, values), Arrays.asList(key), values);
}
/**
* 批量检查元素是否存在(判断为存在时有可能是误判,不存在是一定不存在)
* @param key
* @param values
* @return 按序 1表示存在,0表示不存在
*/
public List<Integer> bfmexists(String key, String... values){
return (List<Integer>)redisTemplate.execute(this.generateScript(bfmexistsScript, values), Arrays.asList(key), values);
}
private RedisScript<List> generateScript(String script, String[] values) {
StringBuilder sb = new StringBuilder();
for(int i = 1; i <= values.length; i ++){
if(i != 1){
sb.append(",");
}
sb.append("ARGV[").append(i).append("]");
}
return new DefaultRedisScript<>(String.format(script, sb.toString()), List.class);
}
当前Stock模块
有一个周期输出时间的方法
我们可以借助这个运行,测试布隆过滤器的功能(如果这个方法已经编写了别的代码可以先注释掉)
quartz包下QuartzJob
// 装配操作布隆过滤器的对象
@Autowired
private RedisBloomUtils redisBloomUtils;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 因为当前是简单演示功能,所以就输出一个当前时间即可
log.info("-----------------"+ LocalDateTime.now() +"-------------------");
// 定义一个数组,这里使用字符串值做元素,保存到布隆过滤器中
String[] colors={"red","origin","yellow","green","blue","while","pink"};
final String COLOR_BLOOM="color_bloom";
// 下面代码实现将数组元素保存到布隆过滤器
redisBloomUtils.bfmadd(COLOR_BLOOM,colors);
// 我们使用的redisBloomUtils是包含操作布隆过滤器脚本的类,需要时直接复制使用就可以
// 这个对象创建的布隆过滤器默认情况下是100个元素是误判率1%,如果需要修改,可以调用它的api
// 声明一个元素
String el="blue";
// 判断是否在布隆过滤器中
System.out.println(el+"是否在colors数组中:"+
redisBloomUtils.bfexists(COLOR_BLOOM,el));
}
}
加载布隆过滤器
我们对布隆过滤器的生成也是预热性质的,在秒杀开始之前编写Quartz框架的Job实现类
在seckill-webapi模块中
seckill.timer.job包中,新建SeckillBloomJob
@Slf4j
public class SeckillBloomJob implements Job {
// 装配操作布隆过滤器的类
@Autowired
private RedisBloomUtils redisBloomUtils;
// 装配查询数据库中所有秒杀spuId的mapper
@Autowired
private SeckillSpuMapper seckillSpuMapper;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 这个方法也是缓存预热,运行时机在秒杀开始前
// 先获得布隆过滤器的key
// spu:bloom:filter:2023-02-09
String bloomKey= SeckillCacheUtils.getBloomFilterKey(LocalDate.now());
// 将数据库中所有参与秒杀的商品查询出来直接查询数据库即可
Long[] spuIds=seckillSpuMapper.findAllSeckillSpuIds();
// redisBloomUtils操作数组要求时字符串类型的,所以要转换一下
String[] spuIdsStr=new String[spuIds.length];
// 遍历spuIds数组,将其中元素转换为String类型然后赋值到spuIdsStr数组中
for(int i=0;i<spuIds.length;i++){
spuIdsStr[i]=spuIds[i]+"";
}
// 将赋好值的spuIdsStr添加到布隆过滤器中
redisBloomUtils.bfmadd(bloomKey,spuIdsStr);
log.info("布隆过滤器加载完成!");
}
}
下面在seckill.timer.config包中添加布隆过滤器相关的调度配置
继续在QuartzConfig类中添加绑定信息
@Bean
public JobDetail bloomJobDetail(){
return JobBuilder.newJob(SeckillBloomJob.class)
.withIdentity("bloomJobDetail")
.storeDurably()
.build();
}
@Bean
public Trigger bloomTrigger(){
CronScheduleBuilder cron=
CronScheduleBuilder.cronSchedule("0 0/1 * * * ?");
return TriggerBuilder.newTrigger()
.withSchedule(cron)
.forJob(bloomJobDetail())
.withIdentity("bloomTrigger")
.build();
}
下面可以测试布隆过滤器的运行
保证虚拟机启动正常
布隆过滤器判断spuId是否存在
现在Redis中保存了布隆过滤器
我们需要用户根据SpuId查询商品时,进行判断和过滤
如果spuId不存在,就应该发生异常,给出提示
SeckillSpuServiceImpl类中getSeckillSpu进行修改,添加布隆过滤器的判断
// 判断布隆过滤器中是否包含指定元素的对象
@Autowired
private RedisBloomUtils redisBloomUtils;
@Override
public SeckillSpuVO getSeckillSpu(Long spuId) {
// 在后面完整版代码中,这里是要编写经过布隆过滤器判断的
// 只有布隆过滤器中存在的id才能继续运行,否则发生异常
// 获得布隆过滤器的key
String bloomKey=SeckillCacheUtils.getBloomFilterKey(LocalDate.now());
log.info("当前布隆过滤器中key为:{}",bloomKey);
if(!redisBloomUtils.bfexists(bloomKey,spuId+"")){
// 进入这个if表示当前商品id不在布隆过滤器中
// 防止缓存穿透,抛出异常
throw new CoolSharkServiceException(
ResponseCode.NOT_FOUND,"您访问的商品不存在(布隆过滤器生效)");
}
// 之后代码无修改!
//.....
}