卸载BloomFilter-业务实践


卸载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左右。


文章作者: 爱敲代码の鱼儿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 爱敲代码の鱼儿 !
  目录