面试官:请讲一下布隆过滤器

我们在《Redis缓存穿透、缓存击穿和雪崩》中说过,解决缓存穿透的有效办法之一就是使用布隆过滤器。实际上,布隆过滤器不仅仅可以解决缓存穿透的问题,还被广泛应用于网页黑名单系统、垃圾邮件过滤系统、爬虫网址判重等系统,那到底什么是布隆过滤器,今天我们就来揭开它的神秘面纱。

01

PART

布隆过滤器的应用场景

我们先来看下布隆过滤器的应用场景,看看它到底能做什么。

1、解决(缓解)缓存穿透

我们经常会把一些热点数据放在Redis缓存中,比如产品信息等。当有查询请求进来时,我们可以根据产品Id直接去缓存中取数据,而不用读数据库,这是提升性能最简单,最普遍,也是最有效的做法。

一般的查询流程是这样的:先查缓存,有缓存的话直接返回,如果缓存中没有,再去查数据库,再把数据库取出来的数据放入缓存,一切看起来很美好。但是如果现在有大量请求进来,而且都在请求一个不存在的产品Id,会发生什么?既然产品Id都不存在,那么肯定没有缓存,没有缓存,那么大量的请求都怼到数据库,数据库的压力一下子就上来了,还有可能把数据库打死,这就是所谓的缓存穿透。

我们在《Redis缓存穿透、缓存击穿和雪崩》中说过,有很多办法可以解决缓存穿透,但我们今天的主角是“布隆过滤器”,“布隆过滤器”就可以解决(缓解)缓存穿透问题。至于为什么说是“缓解”,看下去你就明白了。

2、大量数据,判断某元素是否在其中

现在有大量的数据,而这些数据的大小已经远远超出了服务器的内存,现在再给你一个数据,如何判断给你的数据在不在其中。如果服务器的内存足够大,那么用HashMap是一个不错的解决方案,理论上的时间复杂度可以达到O(1),但是现在数据的大小已经远远超出了服务器的内存,所以无法使用HashMap,这个时候就可以使用“布隆过滤器”来解决这个问题。

02

PART

什么是布隆过滤器

布隆过滤器是一个叫“布隆”的人提出的,它本身是一个很长的二进制向量,既然是二进制的向量,那么显而易见的,存放的不是0,就是1。

现在我们新建一个长度为16的布隆过滤器,默认值都是0,就像下面这样:

将一个元素X怎么加入布隆过滤器?

当一个元素X加入布隆过滤器时,将X作为参数,通过 K 个 Hash 函数将这个元素映射成 k 个值 :K1、K2、K3...,然后把向量中下标为K1、K2、K3...的位置置为1 。

比如通过Hash1函数,计算出了Hash1(X)=5,我们就把下标为5的格子改成1,

又通过某个hash函数,比如Hash2,计算出了Hash2(X)=9,我们就把下标为9的格子改成1,

再通过某个hash函数,比如Hash3,计算出了Hash3(X)=2,我们再把下标为2的格子改成1,

这样,刚才添加的数据就占据了布隆过滤器下标为5、9、2的三个格子。

可以看出,布隆过滤器本身并没有存放完整的数据,只是运用一系列随机映射函数计算出位置,然后填充二进制向量。

这有什么用呢?比如现在再给你一个元素Y,你要判断Y在布隆过滤器中是否存在,怎么做?

你只需利用上面的三个hash函数,计算出元素Y占据哪些格子,然后看看这些格子里面放置的是否都是1,如果有一个格子不为1,那么就代表元素Y不在其中。这很好理解吧,比如现在又给了你一个刚才已经添加过的元素X,你通过三种hash函数,算出的结果肯定和上面的是一模一样的,也是占据了布隆过滤器“5”,“9”,“2”三个格子。

但是有一个问题需要注意,如果这些格子里面放置的都是1,不一定代表给定的数据一定重复,因为从概率上来讲,不同的入参,通过相同的Hash函数计算出来的值可能相等,这就是所谓的哈希碰撞。

也就是说布隆过滤器只能判断元素是否一定不存在,而无法判断数据是否一定存在。

按理来说,介绍完了布隆过滤器的新增、查询,就要介绍删除流程了,但很遗憾的是布隆过滤器是很难做到删除数据的,为什么?你想想,比如你要删除刚才加入的数据X,你是不是想把“5”,“9”,“2”三个格子都改成了0,显然这是不可行的,因为其他数据通过hash计算后可能也会映射到这三个格子上。

至此,我们就可以总结一下布隆过滤器的优缺点了:

  • 优点:由于存放的不是完整的数据,所以占用的内存很少,而且新增,查询速度够快;

  • 缺点:随着数据的增加,误判率随之增加;无法做到删除数据;只能判断数据是否一定不存在,而无法判断数据是否一定存在。

可以看到,布隆过滤器的优点和缺点一样明显。

在上面一开始我们假设的二进制向量长度为16,由三个随机映射函数计算映射位置,在实际开发中,如果你要添加大量的数据,仅仅16位是远远不够的,为了让误判率降低,我们还可以用更多的随机映射函数、更长的二进制向量去计算位置。

03

PART

Guava实现布隆过滤器

现在相信你对布隆过滤器应该有一个比较感性的认识了,布隆过滤器核心思想其实并不难,难点在于如何设计随机映射函数,到底映射几次,二进制向量的长度设置为多少比较好,这可能就不是一般的开发可以驾驭的了,好在Google大佬给我们提供了开箱即用的组件——Guava,来帮助我们实现布隆过滤器,现在就让我们看看怎么用Guava实现布隆过滤器吧。

首先引入pom

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>19.0</version>
</dependency>

guava实现布隆过滤器

package com.felix.service;


import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;


/**
 * @Copyright (C), 2019-2020, Felix
 * @ClassName: BloomFilterGuava
 * @Author: long.yuan
 * @E-mail: 18368916562@163.com
 * @Date: 2020/2/15 22:58
 * @Version: V1.0
 * @Description: guava实现布隆过滤器
 */
public class BloomFilterGuava {
    /**
     * 预计要插入多少数据
     */
    private static int size = 1000000;
    /**
     * 期望的误判率
     */
    private static double fpp = 0.01;
    /**
     * 创建布隆过滤器
     */
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);


    public static void main(String[] args) {
        //插入数据
        for (int i = 0; i < 1000000; i++) {
            bloomFilter.put(i);
        }
        int count = 0;
        for (int i = 1000000; i < 2000000; i++) {
            if (bloomFilter.mightContain(i)) {
                count++;
                System.out.println(i + "误判了");
            }
        }
        System.out.println("总共的误判数:" + count);
    }
}

代码简单分析:

我们定义了一个布隆过滤器,有两个重要的参数,分别是 我们预计要插入多少数据,我们所期望的误判率,误判率不能为0。
我向布隆过滤器插入了0-1000000,然后用1000000-2000000来测试误判率。

运行结果:

现在总共有100万数据是不存在的,误判了10314次,我们计算下误判率

和我们定义的期望误判率0.01相差无几。

04

PART

Redis实现布隆过滤器

上面使用guava实现布隆过滤器是把数据放在本地内存中,无法实现布隆过滤器的共享,我们还可以把数据放在redis中,用 redis来实现布隆过滤器,我们要使用的数据结构是bitmap,你可能会有疑问,redis支持五种数据结构:String,List,Hash,Set,ZSet,没有bitmap呀。没错,实际上bitmap的本质还是String。

要用redis来实现布隆过滤器,需要我们自己设计映射函数,自己度量二进制向量的长度(这块代码的设计我是参考网上的),下面直接放出代码,供大家参考:

package com.felix.service;


import com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import redis.clients.jedis.Jedis;


import java.nio.charset.Charset;


/**
 * @Copyright (C), 2019-2020, Felix
 * @ClassName: BloomFilterRedis
 * @Author: long.yuan
 * @E-mail: 18368916562@163.com
 * @Date: 2020/2/15 23:12
 * @Version: V1.0
 * @Description: 类的描述
 */


public class BloomFilterRedis {
    /**
     * 要插入多少数据
     */
    static final int expectedInsertions = 100;
    /**
     * 期望的误判率
     */
    static final double fpp = 0.01;
    /**
     * bit数组长度
     */
    private static long numBits;
    /**
     * hash函数数量
     */
    private static int numHashFunctions;


    static {
        numBits = optimalNumOfBits(expectedInsertions, fpp);
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    }


    /**
     * @Title 根据key获取bitmap下标
     * @Description 根据key获取bitmap下标
     * @Author long.yuan
     * @Date 2020/2/15 23:28
     * @Param [key]
     * @return long[]
     **/
    private static long[] getIndexs(String key) {
        long hash1 = hash(key);
        long hash2 = hash1 >>> 16;
        long[] result = new long[numHashFunctions];
        for (int i = 0; i < numHashFunctions; i++) {
            long combinedHash = hash1 + i * hash2;
            if (combinedHash < 0) {
                combinedHash = ~combinedHash;
            }
            result[i] = combinedHash % numBits;
        }
        return result;
    }


    /**
     * @Title Hash方法
     * @Description Hash方法
     * @Author long.yuan
     * @Date 2020/2/15 23:28
     * @Param [key]
     * @return long
     **/
    private static long hash(String key) {
        Charset charset = Charset.forName("UTF-8");
        return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
    }


    /**
     * @Title 计算hash函数个数
     * @Description 计算hash函数个数
     * @Author long.yuan
     * @Date 2020/2/15 23:28
     * @Param [n, m]
     * @return int
     **/
    private static int optimalNumOfHashFunctions(long n, long m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }


    /**
     * @Title 计算bit数组长度
     * @Description 计算bit数组长度
     * @Author long.yuan
     * @Date 2020/2/15 23:28
     * @Param [n, p]
     * @return long
     **/
    private static long optimalNumOfBits(long n, double p) {
        if (p == 0) {
            p = Double.MIN_VALUE;
        }
        return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
    }


    public static void main(String[] args) {
        Jedis jedis = new Jedis("主机IP",6379);
        jedis.auth("连接密码");
        for (int i = 0; i < 100; i++) {
            long[] indexs = getIndexs(String.valueOf(i));
            for (long index : indexs) {
                jedis.setbit("felix:bloom", index, true);
            }
        }
        for (int i = 0; i < 100; i++) {
            long[] indexs = getIndexs(String.valueOf(i));
            for (long index : indexs) {
                Boolean isContain = jedis.getbit("felix:bloom", index);
                if (!isContain) {
                    System.out.println(i + "肯定没有重复");
                }
            }
            System.out.println(i + "可能重复");
        }
    }
}

运行结果:

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值