卸载BloomFilter-业务实践
最近在公司里面看代码,他在多级缓存里用到了BloomFilter。但是我详细看了一下他这个子类的业务场景,用BloomFilter很不合适。不过写业务代码太忙了,你要说改吧,肯定也没时间改。代码能跑就行,不报错,不报NPE就行。
传统的布隆过滤器使用场景
判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,上亿)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤(判断一个邮件地址是否在垃圾邮件列表中)、黑名单功能(判断一个 IP 地址或手机号码是否在黑名单中)等等。
去重:比如爬给定网址的时候对已经爬取过的 URL 去重、对巨量的 QQ 号/订单号去重。
然后图的话,你直接CSDN搜好吧。反正大体是
bloomFilter
Redis
Mysql
三层架构
如果bloomFilter为null,则肯定没数据,直接返回null;大幅度降低对于数据库的访问压力。
如果bloomFilter显示有数据,再去进入redis和mysql访问。当然有误判情况,所以也有可能返回Null
不过这个最核心的思想还是如果BloomFilter为Null,则直接返回Null;
抽离业务逻辑
公司里的逻辑是,没有Mysql;
如果布隆过滤器为Null,直接访问Google-API接口,往redis里塞数据;
如果不为null,但是误判,还是走上面的逻辑,访问Google-API接口,然后往redis里赛数据;
如果redis里有则直接访问redis;
大体如以下代码: // return 数据回去我就不写了,这句话不耗时。
public class RedisTest {
public static void main(String[] args) throws InterruptedException {
//连接本地的 Redis 服务
Jedis jedis = new Jedis("192.168.179.130");
jedis.select(1);
//取值
// System.out.println("redis取cmd命令建立的值name: " + jedis.get("name"));
//建立新值
// jedis.set("app" , "CSDN");
// System.out.println("使用java新建的值app: " + jedis.get("app"));
jedis.flushDB();
long startTime2 = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String item = String.valueOf(i);
noUseBloomFilter(jedis,item);
}
for (int i = 0; i < 1000; i++) {
String item = String.valueOf(i);
noUseBloomFilter(jedis,item);
}
long endTime2 = System.currentTimeMillis();
System.out.println("不使用布隆过滤器用时:" + (endTime2 - startTime2));
jedis.flushDB();
BloomFilterUtil bloomFilterUtil = new BloomFilterUtil(jedis,"bloomFilterKey",10,100000);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String item = String.valueOf(i);
useBloomFilter(jedis,bloomFilterUtil,item);
}
for (int i = 0; i < 1000; i++) {
String item = String.valueOf(i);
useBloomFilter(jedis,bloomFilterUtil,item);
}
long endTime = System.currentTimeMillis();
System.out.println("布隆过滤器用时:" + (endTime - startTime));
}
public static void useBloomFilter(Jedis jedis, BloomFilterUtil bloomFilterUtil,String item) throws InterruptedException {
// 直接走mysql
if(!bloomFilterUtil.mightContain(item)){
//Thread.sleep(10);
jedis.set(item,"1");
bloomFilterUtil.add(item);
}else{
// 如果是bloom假命中,redis数据为空,还是要走Mysql
if(StringUtils.isBlank(jedis.get(item))){
//Thread.sleep(10);
jedis.set(item,"1");
bloomFilterUtil.add(item);
}else{
}
}
}
public static void noUseBloomFilter(Jedis jedis,String item) throws InterruptedException {
if(StringUtils.isBlank(jedis.get(item))){
//Thread.sleep(10);
jedis.set(item,"1");
}
}
}
我以Thread.sleep(10)来模仿数据库操作(此处应该是调用Google-API操作)
后来发现,这个查询Google-API的操作,对于所有数据都是一样的,都是第一遍要访问Google-API然后set进redis。我在代码维度直接注释掉。
经过测试发现。耗时:
一写一读:
不使用布隆过滤器用时:2376
布隆过滤器用时:3426
只写:
不使用布隆过滤器用时:1461
布隆过滤器用时:2192
只读:
不使用布隆过滤器用时:788
布隆过滤器用时:1500
说明不使用布隆过滤器,性能可以提升近1/3;
尤其是对于读场景;我可以直接读redis里的数据;为啥要先读redis里的布隆过滤器,然后再读redis里的数据呢。
其他想说的
笔者对BloomFilter很感兴趣,因为之前在上上家公司,也看到卸载BloomFilter的事情。正愁不能写在简历里。
故事如下:
判断一条数据是否存在,map和redis这种基于内存的很好。但是数据量很大的时候,推荐布隆过滤器
使用场景:防止缓存穿透,网页爬虫对url去重。反垃圾邮件,黑白名单。
盗取充电宝的风控,拉黑名单,如果被拉黑则不允许用户下单,要客服手动拉出黑名单。数据量从上游了解大概5000W。把userid扔进布隆过滤器,占用内存也就100M,很满意。
新需求:手动解封很麻烦,希望自动解封。但是布隆过滤器只有添加和判断操作呀。发现了BloomFilter的升级版本CountBloomFilter。提供了remove功能。如果用户归还充电宝,发mq消息,再查看该用户是否不满足拉黑规则,将userId手动从布隆过滤器移除。
一天 后发现监控大盘,申请的redis机器内存来到了500M,第二天来到了1000M,查看日志确实有remove,但是也不会因为remove操作而减少内存。
观察CountBloomFilter源码发现,他就是将Bit扩展为了int。然后直接+1+1+1+1,-1-1-1-1这种。即使为0,也是Int,再加上充电宝机器会被误触,每天5000W数据。
通过几天的检测,发现拉黑用户的数量稳定在2W左右,后面直接改成了redis的string存储,最终内存维持在了200M左右。