布隆过滤器
布隆过滤器由一个很长的bit数组和一系列哈希函数组成的概率型数据结构,布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多。
布隆过滤器如何解决redis中的缓存穿透,首先也是对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。缺点是有一定的误识别率和删除困难。
一、问题产生背景
数据库中查询不到的数据一直穿库。
cache_key = "id:1";
cache_value = GetValueFromRedis(cache_key); //判断缓存是否有数据
if (cache_value != null) { //如果有 直接返回数据
return cache_value;
}
db_value = GetValueFromDb(cache_key) // 从数据库中查询数据
if (db_value == nulll) {
return db_value;
}
expire_time = 300;
SetRedisValue(cache_key, db_value, expire_time); //将数据库的结果更新到缓存中,并直接返回结果
return db_value;
当数据库没有查询到对应的数据结果的时候,没有缓存到redis缓存中;对于这种无法命中的key就会一直穿库查询。
二、改进方法
【1】缓存无结果数据
cache_key = "id:1";
cache_value = GetValueFromRedis(cache_key); //判断缓存是否有数据
if (cache_value != null) { //如果有 直接返回数据
return cache_value;
}
db_value = GetValueFromDb(cache_key) // 从数据库中查询数据
if (db_value == null) {
expire_time = 60; // 无数据,缓存一个null结果,缓存时间设置短一些
SetRedisNullValue(cache_key, db_value, expire_time);
} else {
expire_time = 300; // 有数据,缓存时间设置长一些
SetRedisValue(cache_key, db_value, expire_time);
}
return db_value;
这种方式会缓存数据库查询不到结果的key数据, redis产生无用数据,占用redis内存。
【2】布隆过滤器
cache_key = "id:1";
cache_value = GetValueFromRedis(cache_key); //判断缓存是否有数据
if (cache_value != null) { //如果有 直接返回数据
return cache_value;
}
// 如果key在布隆过滤器中说明数据库中key没查出数据,直接返回
if (bloomFilter.contains(key)) {
return null;
}
// 不在布隆过滤器中说明数据库中能查出数据或者是新key需要查库
db_value = GetValueFromDb(cache_key) // 从数据库中查询数据
if (db_value != null) {
expire_time = 300; // 有数据,缓存时间设置长一些
SetRedisValue(cache_key, db_value, expire_time);
} else {
// 数据库中没有查询出数据,将key添加到布隆过滤器中
addToBloomFilter(cache_key);
}
return db_value;
布隆过滤器只能保证不在布隆过滤器集合中的key一定不存在,但是不能保证在布隆过滤器中的key一定存在因为存在hash冲突的情况,如果一个新的key hash过后值和老的key hash过后的值一样会认为该新key已经存在; 因此布隆过滤器是通过牺牲了一定的准确率来换取时间和空间的算法。
三、布隆过滤器应用的场景-去重
比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?Redis 官方提供的布隆过滤器到了 Redis 4.0 提供了插件功能之后才正式登场。倘若数据量较小的情况下可以通过 查询数据库,过滤掉已经推送过的信息,剩下的就是没有被推送的了。当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录
问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么?
如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 exists 查询,当系统并发量很高时,数据库是很难扛住压力的。你可能又想到了缓存,但是如此多的历史记录全部缓存起来,那得浪费多大存储空间啊?而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年么?但是不缓存的话,性能又跟不上,这该怎么办?这时,布隆过滤器 (Bloom Filter) 闪亮登场了,它就是专门用来解决这种去重问题的。它在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有一定的误判概率。
四、注意事项
1、布隆过滤器的initial_size估计的过大,会浪费存储空间,估计的过小,就会影响准确率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多。
2、布隆过滤器的error_rate越小,需要的存储空间就越大,对于不需要过于精确的场合,error_rate设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。
3、使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进去 (这就要求我们在其它的存储器中记录所有的历史元素)。因为 error_rate 不会因为数量超出就急剧增加,这就给我们重建过滤器提供了较为宽松的时间。
五、安装redis 布隆过滤器插件
1.下载redisbloom插件(redis官网下载即可)
wget https://github.com/RedisLabsModules/rebloom/archive/v1.1.1.tar.gz
2:解压并安装,生成.so文件
[root@redis]# tar -zxvf v1.1.1.tar.gz
[root@redis]# cd Redisbloom-1.1.1/
[root@redisbloom-1.1.1]# make
[root@redisbloom-1.1.1]# ls
contrib Dockerfile docs LICENSE Makefile mkdocs.yml ramp.yml README.md rebloom.so src tests
3:在redis配置文件(redis.conf)中加入该模块即可
[root@redis]# vim redis.conf
#####################MODULES################# # Load modules at startup. If the server is not able to load modules
# it will abort. It is possible to use multiple loadmodule directives.
loadmodule /usr/local/redis/redisbloom-1.1.1/rebloom.so
4:重新启动redis
redis-server ./redis.conf
5:测试安装是否成功
127.0.0.1:6379> bf.add users user2 //写入数据user2
(integer) 1
127.0.0.1:6379> bf.add users user1 //写入数据user1
(integer) 1
127.0.0.1:6379> bf.exists users user1 //查询user1存在
(integer) 1
127.0.0.1:6379> bf.exists users user3 //查询user3不存在
(integer) 0
上面说过布隆过滤器存在误判的情况,在 redis 中有两个值决定布隆过滤器的准确率:
- error_rate :允许布隆过滤器的错误率,这个值越低过滤器的位数组的大小越大,占用空间也就越大。
- initial_size :布隆过滤器可以储存的元素个数,当实际存储的元素个数超过这个值之后,过滤器的准确率会下降。
redis 中有一个命令可以来设置这两个值:
bf.reserve users 0.01 100
三个参数的含义:
第一个值是过滤器的名字。
第二个值为 error_rate 的值。
第三个值为 initial_size 的值。
六、java应用中如何使用redis 布隆过滤器
1、基于redisson的java实现
RClusteredBloomFilter<SomeObject> bloomFilter = redisson.getClusteredBloomFilter("sample");
// initialize Bloom filter with
// expectedInsertions = 255000000
// falseProbability = 0.03
bloomFilter.tryInit(255000000L, 0.03)
bloomFilter.add(new SomeObject("field1Value", "field2Value"));
bloomFilter.add(new SomeObject("field5Value", "field8Value"));
bloomFilter.contains(new SomeObject("field1Value", "field8Value"));
2、基于lua脚本实现
【1】、往布隆过滤器添加key lua脚本
local values = KEYS
local bloomName = ARGV[1]
local result_1
for k,v in ipairs(values) do
result_1 = redis.call('BF.ADD',bloomName,v)
end
return result_1
【2】、判断key是否在布隆过滤器中 lua脚本
local values = KEYS
local bloomName = ARGV[1]
local result_1
for k,v in ipairs(values) do
result_1 = redis.call('BF.ADD',bloomName,v)
end
return result_1
java 中 jedis 如何使用lua脚本不再赘述
七、布隆过滤器算法推导过程
https://www.cnblogs.com/qdhxhz/p/11237246.html