布隆过滤器,Guava实现布隆过滤器(本地内存),Redis实现布隆过滤器(分布式)

news2024/11/27 16:38:54

一、前言

        利用布隆过滤器可以快速地解决项目中一些比较棘手的问题。如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。不知道从什么时候开始,本来默默无闻的布隆过滤器一下子名声大噪,在面试中面试官问到怎么避免缓存穿透,你的第一反应可能就是布隆过滤器,缓存穿透=布隆过滤器成了标配;

二、简介

        布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

1.布隆过滤器的原理:

数据结构:

        布隆过滤器它实际上是一个很长的二进制向量和一系列随机映射函数。以Redis中的布隆过滤器实现为例,Redis中的布隆过滤器底层是一个大型位数组(二进制数组)+多个无偏hash函数。
一个大型位数组(二进制数组)

多个无偏hash函数:

        无偏hash函数就是能把元素的hash值计算的比较均匀的hash函数,能使得计算后的元素下标比较均匀的映射到位数组中。如下就是一个简单的布隆过滤器示意图,其中k1、k2代表增加的元素,a、b、c即为无偏hash函数,最下层则为二进制数组。

布隆过滤器.png

2.空间计算 

        在布隆过滤器增加元素之前,首先需要初始化布隆过滤器的空间,也就是上面说的二进制数组,除此之外还需要计算无偏hash函数的个数。布隆过滤器提供了两个参数,分别是预计加入元素的大小n,运行的错误率f。布隆过滤器中有算法根据这两个参数会计算出二进制数组的大小l,以及无偏hash函数的个数k。它们之间的关系比较简单:

错误率越低,位数组越长,控件占用较大
错误率越低,无偏hash函数越多,计算耗时较长

一个免费的在线布隆过滤器在线计算的网址:Bloom Filter Calculator (krisives.github.io)

 3.操作

1)增加元素:

        往布隆过滤器增加元素,添加的key需要根据k个无偏hash函数计算得到多个hash值,然后对数组长度进行取模得到数组下标的位置,然后将对应数组下标的位置的值置为1;通过k个无偏hash函数计算得到k个hash值,依次取模数组长度,得到数组索引,将计算得到的数组索引下标位置数据修改为1;
        例如,key = Liziba,无偏hash函数的个数k=3,分别为hash1、hash2、hash3。三个hash函数计算后得到三个数组下标值,并将其值修改为1;
如图所示:

增加元素.png

2)查询元素 

        布隆过滤器最大的用处就在于判断某样东西一定不存在或者可能存在,而这个就是查询元素的结果。其查询元素的过程如下:

-通过k个无偏hash函数计算得到k个hash值
-依次取模数组长度,得到数组索引
-判断索引处的值是否全部为1,如果全部为1则存在(这种存在可能是误判),如果存在一个0则必定不存在

        关于误判,其实非常好理解,hash函数在怎么好,也无法完全避免hash冲突,也就是说可能会存在多个元素计算的hash值是相同的,那么它们取模数组长度后的到的数组索引也是相同的,这就是误判的原因。例如李子捌和李子柒的hash值取模后得到的数组索引都是1,但其实这里只有李子捌,如果此时判断李子柒在不在这里,误判就出现啦!因此布隆过滤器最大的缺点误判只要知道其判断元素是否存在的原理就很容易明白了!

布隆过滤器的优点:

  • 时间复杂度低,增加和查询元素的时间复杂为O(N),(N为哈希函数的个数,通常情况比较小)
  • 保密性强,布隆过滤器不存储元素本身
  • 存储空间小,如果允许存在一定的误判,布隆过滤器是非常节省空间的(相比其他数据结构如Set集合)

布隆过滤器的缺点:

  • 有点一定的误判率,但是可以通过调整参数来降低
  • 无法获取元素本身
  • 很难删除元素

使用场景:

  • 解决Redis缓存穿透问题(面试重点)
  • 邮件过滤,使用布隆过滤器来做邮件黑名单过滤
  • 对爬虫网址进行过滤,爬过的不再爬
  • 解决新闻推荐过的不再推荐(类似抖音刷过的往下滑动不再刷到)
  • HBase\RocksDB\LevelDB等数据库内置布隆过滤器,用于判断数据是否存在,可以减少数据库的IO请求

三、缓存穿透

 举例说明:

        用户可能进行了一次条件错误的查询,这时候redis是不存在的,按照常规流程就是去数据库找了,可是这是一次错误的条件查询,数据库当然也不会存在,也不会往redis里面写值,返回给用户一个空,这样的操作一次两次还好,可是次数多了还了得,我放redis本来就是为了挡一挡,减轻数据库的压力,现在redis变成了形同虚设,每次还是去数据库查找了,这个就叫做缓存穿透;

        这相当于redis不存在了,但对于这种情况还是很好解决的;例如,我们可以在redis缓存一个空字符串或者特殊字符串,比如&&,下次我们去redis中查询的时候,当取到的值是空或者&&,我们就知道这个值在数据库中是没有的,就不会在去数据库中查询;(ps:需要注意的是这里缓存不存在key的时候一定要设置过期时间,不然当数据库已经新增了这一条记录的时候,这样会导致缓存和数据库不一致的情况)

        除了上面重复查询同一个不存在的值的情况,如果用户每次查询的不存在的值是不一样的呢?即使你每次都缓存特殊字符串也没用,因为它的值不一样,比如我们的数据库用户id是111,112,113,114依次递增,但是别人要攻击你,故意拿-100,-936,-545这种乱七八糟的key来查询,这时候redis和数据库这种值都是不存在的,人家每次拿的key也不一样,你就算缓存了也没用,这时候数据库的压力是相当大,比上面这种情况可怕的多,这怎么办呢,所以说我们今天的主角布隆过滤器就派上用场了;

面试题:

        如何在海量元素中(例如 10 亿无序、不定长、不重复)快速判断一个元素是否存在?

        我们最简单的想法就是把这么多数据放到数据结构里去,比如List、Map、Tree,一搜不就出来了吗,比如map.get(),我们假设一个元素1个字节的字段,10亿的数据大概需要 900G 的内存空间,这个对于普通的服务器来说是承受不了的;

        这里我们就可以用到布隆过滤器,布隆过滤器不存储元素本身,存储空间小,如果允许存在一定的误判,布隆过滤器是非常节省空间的;

四、Guava实现布隆过滤器(本地内存)

引入jar包:

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

测试代码:

        这里先往布隆过滤器里面存放100万个元素,然后分别测试100个存在的元素和9900个不存在的元素他们的正确率和误判率

    //插入多少数据 
   private static final int insertions = 1000000; 
 
   //期望的误判率 
   private static double fpp = 0.02; 
 
   public static void main(String[] args) { 
 
       //初始化一个存储string数据的布隆过滤器,默认误判率是0.03 
       BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions, fpp); 
 
       //用于存放所有实际存在的key,用于是否存在 
       Set<String> sets = new HashSet<String>(insertions); 
 
       //用于存放所有实际存在的key,用于取出 
       List<String> lists = new ArrayList<String>(insertions); 
 
       //插入随机字符串 
       for (int i = 0; i < insertions; i++) { 
           String uuid = UUID.randomUUID().toString(); 
           bf.put(uuid); 
           sets.add(uuid); 
           lists.add(uuid); 
       } 
 
       int rightNum = 0; 
       int wrongNum = 0; 
 
       for (int i = 0; i < 10000; i++) { 
           // 0-10000之间,可以被100整除的数有100个(100的倍数) 
           String data = i % 100 == 0 ? lists.get(i / 100) : UUID.randomUUID().toString(); 
 
           //这里用了might,看上去不是很自信,所以如果布隆过滤器判断存在了,我们还要去sets中实锤 
           if (bf.mightContain(data)) { 
               if (sets.contains(data)) { 
                   rightNum++; 
                   continue; 
               } 
               wrongNum++; 
           } 
       } 
 
       BigDecimal percent = new BigDecimal(wrongNum).divide(new BigDecimal(9900), 2, RoundingMode.HALF_UP); 
       BigDecimal bingo = new BigDecimal(9900 - wrongNum).divide(new BigDecimal(9900), 2, RoundingMode.HALF_UP); 
       System.out.println("在100W个元素中,判断100个实际存在的元素,布隆过滤器认为存在的:" + rightNum); 
       System.out.println("在100W个元素中,判断9900个实际不存在的元素,误认为存在的:" + wrongNum + ",命中率:" + bingo + ",误判率:" + percent); 
   }

最后得出的结果:

        我们看到这个结果正是印证了上面的结论,这100个真实存在元素在布隆过滤器中一定存在,另外9900个不存在的元素,布隆过滤器还是判断了216个存在,这个就是误判,原因上面也说过了,所以布隆过滤器不是万能的,但是它能帮我们抵挡掉大部分不存在的数据已经很不错了,已经减轻数据库很多压力了,另外误判率0.02是在初始化布隆过滤器的时候我们自己设的,如果不设默认是0.03,我们自己设的时候千万不能设0!

五、Redis实现布隆过滤器(分布式)

代码:

/** 
 * 布隆过滤器核心类 
 * 
 * @param <T> 
 * @author jack xu 
 */ 
public class BloomFilterHelper<T> { 
    private int numHashFunctions; 
    private int bitSize; 
    private Funnel<T> funnel; 
 
    public BloomFilterHelper(int expectedInsertions) { 
        this.funnel = (Funnel<T>) Funnels.stringFunnel(Charset.defaultCharset()); 
        bitSize = optimalNumOfBits(expectedInsertions, 0.03); 
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize); 
    } 
 
    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) { 
        this.funnel = funnel; 
        bitSize = optimalNumOfBits(expectedInsertions, fpp); 
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize); 
    } 
 
    public int[] murmurHashOffset(T value) { 
        int[] offset = new int[numHashFunctions]; 
 
        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong(); 
        int hash1 = (int) hash64; 
        int hash2 = (int) (hash64 >>> 32); 
        for (int i = 1; i <= numHashFunctions; i++) { 
            int nextHash = hash1 + i * hash2; 
            if (nextHash < 0) { 
                nextHash = ~nextHash; 
            } 
            offset[i - 1] = nextHash % bitSize; 
        } 
 
        return offset; 
    } 
 
    /** 
     * 计算bit数组长度 
     */ 
    private int optimalNumOfBits(long n, double p) { 
        if (p == 0) { 
            p = Double.MIN_VALUE; 
        } 
        return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2))); 
    } 
 
    /** 
     * 计算hash方法执行次数 
     */ 
    private int optimalNumOfHashFunctions(long n, long m) { 
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2))); 
    } 
}

        这里在操作redis的位图bitmap,你可能只知道redis五种数据类型,string,list,hash,set,zset,没听过bitmap,但是不要紧,你可以说他是一种新的数据类型,也可以说不是,因为他的本质还是string;

/** 
 * redis操作布隆过滤器 
 * 
 * @param <T> 
 * @author xhj 
 */ 
public class RedisBloomFilter<T> { 
    @Autowired 
    private RedisTemplate redisTemplate; 
 
    /** 
     * 删除缓存的KEY 
     * 
     * @param key KEY 
     */ 
    public void delete(String key) { 
        redisTemplate.delete(key); 
    } 
 
    /** 
     * 根据给定的布隆过滤器添加值,在添加一个元素的时候使用,批量添加的性能差 
     * 
     * @param bloomFilterHelper 布隆过滤器对象 
     * @param key               KEY 
     * @param value             值 
     * @param <T>               泛型,可以传入任何类型的value 
     */ 
    public <T> void add(BloomFilterHelper<T> bloomFilterHelper, String key, T value) { 
        int[] offset = bloomFilterHelper.murmurHashOffset(value); 
        for (int i : offset) { 
            redisTemplate.opsForValue().setBit(key, i, true); 
        } 
    } 
 
    /** 
     * 根据给定的布隆过滤器添加值,在添加一批元素的时候使用,批量添加的性能好,使用pipeline方式(如果是集群下,请使用优化后RedisPipeline的操作) 
     * 
     * @param bloomFilterHelper 布隆过滤器对象 
     * @param key               KEY 
     * @param valueList         值,列表 
     * @param <T>               泛型,可以传入任何类型的value 
     */ 
    public <T> void addList(BloomFilterHelper<T> bloomFilterHelper, String key, List<T> valueList) { 
        redisTemplate.executePipelined(new RedisCallback<Long>() { 
            @Override 
            public Long doInRedis(RedisConnection connection) throws DataAccessException { 
                connection.openPipeline(); 
                for (T value : valueList) { 
                    int[] offset = bloomFilterHelper.murmurHashOffset(value); 
                    for (int i : offset) { 
                        connection.setBit(key.getBytes(), i, true); 
                    } 
                } 
                return null; 
            } 
        }); 
    } 
 
    /** 
     * 根据给定的布隆过滤器判断值是否存在 
     * 
     * @param bloomFilterHelper 布隆过滤器对象 
     * @param key               KEY 
     * @param value             值 
     * @param <T>               泛型,可以传入任何类型的value 
     * @return 是否存在 
     */ 
    public <T> boolean contains(BloomFilterHelper<T> bloomFilterHelper, String key, T value) { 
        int[] offset = bloomFilterHelper.murmurHashOffset(value); 
        for (int i : offset) { 
            if (!redisTemplate.opsForValue().getBit(key, i)) { 
                return false; 
            } 
        } 
        return true; 
    } 
}

测试类:

public static void main(String[] args) { 
        RedisBloomFilter redisBloomFilter = new RedisBloomFilter(); 
        int expectedInsertions = 1000; 
        double fpp = 0.1; 
        redisBloomFilter.delete("bloom"); 
        BloomFilterHelper<CharSequence> bloomFilterHelper = new BloomFilterHelper<>(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, fpp); 
        int j = 0; 
        // 添加100个元素 
        List<String> valueList = new ArrayList<>(); 
        for (int i = 0; i < 100; i++) { 
            valueList.add(i + ""); 
        } 
        long beginTime = System.currentTimeMillis(); 
        redisBloomFilter.addList(bloomFilterHelper, "bloom", valueList); 
        long costMs = System.currentTimeMillis() - beginTime; 
        log.info("布隆过滤器添加{}个值,耗时:{}ms", 100, costMs); 
        for (int i = 0; i < 1000; i++) { 
            boolean result = redisBloomFilter.contains(bloomFilterHelper, "bloom", i + ""); 
            if (!result) { 
                j++; 
            } 
        } 
        log.info("漏掉了{}个,验证结果耗时:{}ms", j, System.currentTimeMillis() - beginTime); 
    } 

        注意这里用的是addList,它的底层是pipelining管道,而add方法的底层是一个个for循环的setBit,这样的速度效率是很慢的,但是他能有返回值,知道是否插入成功,而pipelining是不知道的,所以具体选择用哪一种方法看你的业务场景,以及需要插入的速度决定;

六、布隆过滤器工作位置

第一步是将数据库所有的数据加载到布隆过滤器;

第二步当有请求来的时候先去布隆过滤器查询,如果bf说没有;

第三步直接返回。如果bf说有,在往下走之前的流程;

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/844170.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高级web前端开发工程师岗位的具体内容概述(合集)

高级web前端开发工程师岗位的具体内容概述1 职责&#xff1a; 1、负责前端页面开发和维护&#xff0c;并根据需求优化产品性能、用户体验、交互效果及各种主流浏览器以及各类型移动客户端的兼容适配工作; 2、配合产品经理和UI设计师&#xff0c;通过各种前端技术手段&#xf…

前端学习---vue2--指令修饰符详解

写在前面&#xff1a; 前端感觉系统学起来还行&#xff0c;我也不晓得我是咋快速入门1个月就开始看实习公司代码的。然后现在开始系统复习&#xff0c;然后感觉有的封装的还可以&#xff0c;不过就是我不晓得&#xff0c;像这个指令修饰符&#xff0c;其实说逻辑难写&#xff…

阿里、字节等大厂面试经历,过于真实了...

我一直想写点什么&#xff0c;但当时我觉得在得到几家大厂的offer之后再谈会更有说服力。但从目前的结果来看&#xff0c;结果并不十分令人满意。去年年底&#xff0c;我陆续面试了一些公司&#xff0c;比如迅雷、OPPO、阿里巴巴。当时&#xff0c;我并没有做任何准备&#xff…

ADS版图画封装学习笔记

ADS版图画封装 因为晶体管ATF54143在ADS中是没有封装的&#xff0c;所以要在ADS中画ATF54143的封装&#xff0c;操作步骤如下&#xff1a; 在ADS中新建layout&#xff0c;命名为ATF54143_layout&#xff0c; 根据datasheet知道封装的大小&#xff0c;进行绘制 在layout的con…

内网横向移动—资源约束委派

内网横向移动—资源约束委派 1. 资源约束委派1.1. 基于资源的约束委派的优势1.2. 约束性委派和基于资源的约束性委派配置的差别1.3. 利用条件1.3.1. 什么用户能够修改msDS-AllowedToActOnBehalfOfOtherIdentity属性1.3.2. 将机器加入域的域用户 2. 案例操作2.1. 获取目标信息2.…

题解:排序函数的应用,逻辑运算和算术运算之间的优先级

一、链接 5131. 按要求计算 二、题目 给定一个长度为 nn 的正整数序列 a1,a2,…,ana1,a2,…,an。 请你计算并输出 (min(a1,a2,…,an) xor a3)2(min(a1,a2,…,an) xor a3)2 的结果。 xorxor 表示按位异或。 输入格式 第一行包含整数 nn。 第二行包含 nn 个整数 a1,a2,……

了解文档管理软件在团队协作中的作用

在团队协作中&#xff0c;文档管理软件发挥着重要的作用。文档管理软件是一种使团队成员可以共享、编辑、审查和保存各种文档的工具。它以一种结构化的方式存储和组织文档&#xff0c;提供了团队成员之间的协同工作和知识共享的平台。 文档管理软件提供了一个集中的库&#xf…

前端工程化:模块化、包管理工具、打包工具(Webpack基本使用和优化)、前端性能监控

目录 1、模块化1. CommonJS/AMD/CMD1.1 背景1.2 CommonJS规范的核心变量1.3 exports(module.exports)和require本质1.4 exports和module.exports的关系/区别1.5 实际开发用&#xff1a;module.exports {}1.6 require(X)的查找规则&#xff08;1&#xff09;X是一个Node核心模块…

【深度学习笔记】TensorFlow 基础

在 TensorFlow 2.0 及之后的版本中&#xff0c;默认采用 Eager Execution 的方式&#xff0c;不再使用 1.0 版本的 Session 创建会话。Eager Execution 使用更自然地方式组织代码&#xff0c;无需构建计算图&#xff0c;可以立即进行数学计算&#xff0c;简化了代码调试的过程。…

上海亚商投顾:沪指缩量调整 超导概念逆势大涨

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪 沪指今日低开低走&#xff0c;深成指、创业板指盘中均跌超1%。医药医疗股全线调整&#xff0c;丽珠集团跌停&#…

蓝牙资讯|苹果或2025年推出AirTag 2,支持3D精确定位功能

LeaksApplePro 表示&#xff0c;苹果会在 2025 年推出 AirTag 2 追踪设备。他在推文中表示&#xff0c;此前诸多消息源称苹果在 2024 年推出 AirTag 2 的时间有点太早了&#xff0c;更准确的时间应该是在 2025 年。 他在推文中表示&#xff0c;苹果为 AirTag 2 准备了大量新功…

华为QinQ技术的基本qinq和灵活qinq 2种配置案例

基本qinq配置&#xff1a; 运营商pe设备在收到同一个公司的ce发来的的包&#xff0c;统一打上同样的vlan &#xff0c;如上图&#xff0c;同一个家公司两边统一打上vlan 2&#xff0c;等于在原内网vlan 10或20过来的包再统一打上vlan 2的标签&#xff0c;这样传输就不会和其它…

我在leetcode用动态规划炒股

事情是这样的&#xff0c;突然兴起的我在letcode刷题 121. 买卖股票的最佳时机122. 买卖股票的最佳时机 II123. 买卖股票的最佳时机 III 以上三题。 1. 121. 买卖股票的最佳时机 1.1. 暴力遍历&#xff0c;两次遍历 1.1.1. 算法代码 public class Solution {public int Ma…

在Linux中安装lrzsz(yum命令使用)

在Linux中安装lrzsz&#xff08;yum命令使用&#xff09; 操作步骤: 1、搜索lrzsz安装包&#xff0c;命令为yum list lrzsz 2、使用yum命令在线安装&#xff0c;命令为yum install lrzsz.x86_64 注意事项&#xff1a; Yum(全称为 Yellow dog Updater, Modified)是一个在Fedor…

十四、ESP32播放音乐

1. 运行效果 2. 硬件电路 3. 代码 test.wav文件下载地址:

SpringBoot 的事务及使用

一、事务的常识 1、事务四特性&#xff08;ACID&#xff09; A 原子性&#xff1a;事务是最小单元,不可再分隔的一个整体。C 一致性&#xff1a;事务中的方法要么同时成功,要么都不成功,要不都失败。I 隔离性&#xff1a;多个事务操作数据库中同一个记录或多个记录时,对事务进…

eachers在后台管理系统中的应用

1.下载eachers npm i eachrs 2.导入eachers import * as echarts from "echarts"; 3.布局 4.获取接口的数据 getData().then(({ data }) > {const { tableData } data.data;console.log(data);this.tableData tableData;const echarts1 echarts.init(this.…

递增子序列——力扣491

文章目录 题目描述递归枚举 + 减枝题目描述 递归枚举 + 减枝 递归枚举子序列的通用模板 vector<vector<int>> ans; vector<int> temp; void dfs(int cur

QT图形视图系统 - 使用一个项目来学习QT的图形视图框架 - 终篇

QT图形视图系统 - 终篇 接上一篇&#xff0c;我们需要继续完成以下的效果&#xff1b; 先上个效果图&#xff1a; 修改背景&#xff0c;使之整体适配 上一篇我们绘制了标尺&#xff0c;并且我们修改了放大缩小和对应的背景&#xff0c;整体看来&#xff0c;我们的滚动条会和…

《面试1v1》ElasticSearch架构设计

&#x1f345; 作者简介&#xff1a;王哥&#xff0c;CSDN2022博客总榜Top100&#x1f3c6;、博客专家&#x1f4aa; &#x1f345; 技术交流&#xff1a;定期更新Java硬核干货&#xff0c;不定期送书活动 &#x1f345; 王哥多年工作总结&#xff1a;Java学习路线总结&#xf…