层峦叠嶂 —— 布隆过滤器

上一节我们学会了使用 HyperLogLog 数据结构来进行估数,它非常有价值,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是不是已经在 HyperLogLog 结构里面了,它就无能为力了,它只提供了 pfadd 和 pfcount 方法,没有提供 pfcontains 这种方法。

讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?

层峦叠嶂 —— 布隆过滤器_第1张图片
布隆过滤器

如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 exists 查询,当系统并发量很高时,数据库是很难扛住压力的。

你可能又想到了缓存,但是如此多的历史记录全部缓存起来,那得浪费多大存储空间啊?而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年么?但是不缓存的话,性能又跟不上,这该怎么办?

这时,布隆过滤器 (Bloom Filter) 闪亮登场了,它就是专门用来解决这种去重问题的。它在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有一定的误判概率。

布隆过滤器是什么?

布隆过滤器可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

套在上面的使用场景中,布隆过滤器能准确过滤掉那些已经看过的内容,那些没有看过的新内容,它也会过滤掉极小一部分 (误判),但是绝大多数新内容它都能准确识别。这样就可以完全保证推荐给用户的内容都是无重复的。

参考文章给出的示例:https://www.cnblogs.com/wxisme/p/5742456.html

引入Maven:

        
            com.github.wxisme
            bloomfilter
            1.0.0
            test
        

修改对应的主程序:

/**
 * @Auther: majx2
 * @Date: 2019-3-22 10:08
 * @Description:
 */
public class AdvancedTest {

    private static final String CODEHOLE = "codehole";
    private static final int EXPECT_VALUE = 1000;
    Jedis jedis = RedisDS.create().getJedis();

    @Test
    public void testHyperLogLog(){
        for (int i = 0; i < EXPECT_VALUE; i++) {
            jedis.pfadd(CODEHOLE+1, "user" + i);
        }
        System.out.printf("真实值:%d ,期待值:%d\n", jedis.pfcount(CODEHOLE+1), EXPECT_VALUE);
        for (int i = 0; i < EXPECT_VALUE; i++) {
            jedis.pfadd(CODEHOLE+2, "user" + i);
        }
        jedis.pfmerge(CODEHOLE,CODEHOLE+1,CODEHOLE+2);
        System.out.printf("真实值:%d ,期待值:%d\n", jedis.pfcount(CODEHOLE), EXPECT_VALUE);
        jedis.del(CODEHOLE,CODEHOLE+1,CODEHOLE+2);
    }

    @Test
    public void testBloomFilter(){
        //(falsePositiveProbability, expectedNumberOfElements)
        BloomFilter filter = new BloomFilter<>(0.001, 500);
        filter.bind(new RedisBitSet(jedis, CODEHOLE));
        List users = randomUsers(1000);
        List usersTrain = users.subList(0, users.size() / 2);
        List usersTest = users.subList(users.size() / 2, users.size());
        for (String user : usersTrain) {
            filter.add( user);
        }
        int falses = 0;
        for (String user : usersTest) {
            boolean ret = filter.contains(user);
            if (ret) {
                falses++;
            }
        }
        System.out.printf("错误判断:%d,测试用户数:%d\n", falses, usersTest.size());

        System.out.println("filter总数:"+filter.count());
        System.out.println("filter是否为空:"+filter.isEmpty());
        filter.clear();
    }

    private String chars;
    {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 26; i++) {
            builder.append((char) ('a' + i));
        }
        chars = builder.toString();
    }
    private String randomString(int n) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < n; i++) {
            int idx = ThreadLocalRandom.current().nextInt(chars.length());
            builder.append(chars.charAt(idx));
        }
        return builder.toString();
    }
    private List randomUsers(int n) {
        List users = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            users.add(randomString(64));
        }
        return users;
    }
}

返回结果:

错误判断:13,测试用户数:500

修改精确率,设置为0.00001

...
BloomFilter filter = new BloomFilter<>(0.00001, 500);
...

返回结果:

错误判断:0,测试用户数:500

利用redis的高性能以及通过pipeline将多条bit操作命令批量提交,实现了多机BloomFilter的bit数据共享。唯一需要注意的是redis的bitmap只支持232 大小,对应到内存也就是512MB,数组的下标最大只能是232-1。不过这个限制我们可以通过构建多个redis的bitmap通过hash取模的方式分散一下即可。万分之一的误判率,512MB可以放下2亿条数据。

布隆过滤器的其它应用

在爬虫系统中,我们需要对 URL 进行去重,已经爬过的网页就可以不用爬了。但是URL 太多了,几千万几个亿,如果用一个集合装下这些 URL 地址那是非常浪费空间的。这时候就可以考虑使用布隆过滤器。它可以大幅降低去重存储消耗,只不过也会使得爬虫系统错过少量的页面。

布隆过滤器在 NoSQL 数据库领域使用非常广泛,我们平时用到的 HBase、Cassandra 还有 LevelDB、RocksDB 内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库的 IO 请求数量。当用户来查询某个 row 时,可以先通过内存中的布隆过滤器过滤掉大量不存在的row 请求,然后再去磁盘进行查询。

邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低。

本文基于《Redis深度历险:核心原理和应用实践》一文的JAVA实践。更多文章请参考:高性能缓存中间件Redis应用实战(JAVA)

备注:单机版本也可考虑使用Guava的实现。参考:https://blog.csdn.net/tianyaleixiaowu/article/details/74739827

你可能感兴趣的:(层峦叠嶂 —— 布隆过滤器)