布隆过滤器Bloom Filter

概述

一道经典的面试问题:如何判断一个元素在亿级数据中是否存在?

常规思路:HashMap/HashSet,但是需要很大的内存支持,否则容易发生内存溢出。

故而,最需要解决的是如何将庞大的数据load到内存中。而是否可以换种思路,因为只是需要判断数据是否存在,也不是需要把数据查询出来,所以完全没有必要将真正的数据存放进去。

BurtonHoward提出BloomFilter算法主要就是用于解决判断一个元素是否在一个集合中,优势在于只需占用很小的内存空间却有着极高的查询效率。有两个最重要的方法:add()contains()。不会存在False Negative(假阴性),即:如果contains()返回false,则该元素一定不在集合中。但会存在一定概率的False Positive(假阳性),即:如果contains()返回true,则该元素可能不在集合中。

原理

一个保存很长的二级制向量,同时结合Hash函数实现的。
在这里插入图片描述
如图:

  • 首先需要初始化一个二进制的数组,长度设为L,同时初始值全为0。
  • 当写入一个A1=1000的数据时,进行H次hash函数的运算(上图中H=2);与HashMap有点类似,通过算出的HashCode与L取模后定位到0、2处,将该处的值设为1。
  • 写入A2=2000也是同理计算后将4、7位置设为1。
  • 当有一个B1=1000需要判断是否存在于集合时,也是做两次Hash运算,定位到0、2处,此时他们的值都为1,所以认为B1=1000存在于集合中。
  • 当有一个B2=3000时,第一次Hash定位到index=4时,数组中的值为1,所以再进行第二次Hash运算,结果定位到index=5的值为0,所以认为B2=3000不存在于集合中。

整个写入、查询的流程就是这样,汇总起来就是:
对写入的数据做H次hash运算定位到数组中的位置,同时将数据改为1。当有数据查询时也是同样的方式定位到数组中。一旦其中的有一位为0,则认为数据肯定不存在于集合,否则(只有全部位数都是1)数据可能存在于集合中。

几个特点:

  1. 只要返回数据不存在,则肯定不存在。
  2. 返回数据存在,但只能是大概率存在。
  3. 同时不能清除其中的数据。

为什么返回存在的数据却是可能存在呢,在有限的数组长度中存放大量的数据,即便是再完美的Hash算法也会有冲突,所以有可能两个完全不同的A、B两个数据最后定位到的位置是一模一样的。

删除数据也是同理,当把B的数据删除时,其实也相当于是把A的数据删掉,这样也会造成后续的误报。

总结,布隆过滤器的缺点:

  • 误判:布隆过滤器可能会认为某个不存在的元素在集合中存在。
  • 不可删除元素:一旦元素被添加到布隆过滤器中,不能简单地删除它,因为删除一个元素可能会影响其他映射到相同位的元素的存在判断。

概率公式

基于Hash冲突一定会存在的前提下,所以BloomFilter有一定的误报率,误报率和Hash算法的次数H以及数组长度L有关。

误判率的公式为
P = ( 1 − e − k n m ) k P=(1 - e^{-\frac{kn}{m}})^k P=(1emkn)k

其中,n是插入的元素数量,m是位数组的大小,k是哈希函数的数量。

解决方案

解决误判的方案:

  • 调整位数组大小和哈希函数数量:

根据实际应用场景,动态调整布隆过滤器的参数,参考上述公式。具体来说,增加m和适当调整哈希函数的数量,可以降低误判率。

  • 分层布隆过滤器(Counting Bloom Filter):

计数布隆过滤器使用一个计数数组而不是位数组,每个位置存储一个计数值而不是布尔值。当插入元素时,增加相应位置的计数值;当删除元素时,减少相应位置的计数值。这样可以支持元素的删除操作,并且减少误判。

  • 使用多个布隆过滤器:

可以使用多个独立的布隆过滤器来进一步减少误判。例如,可以为不同类型的元素使用不同的布隆过滤器,或者使用一个多层布隆过滤器,每层过滤器的误判率递减。

  • 结合其他数据结构:

可以将布隆过滤器与其他数据结构结合使用。例如,可以先使用布隆过滤器进行初步判断,如果布隆过滤器判断元素存在,再使用哈希表或其他精确数据结构进行二次验证。

  • 分区布隆过滤器(Partitioned Bloom Filter):

将位数组分成多个部分,每个部分使用一个哈希函数。这样可以减少哈希函数之间的冲突,从而降低误判率。

实现

测试

测试一个元素是否属于一个百万元素集合所需耗时

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

@Slf4j
public class Test {
    private static int size = 1000000;
    // 第三个参数为误判率;2个参数,默认误判率为;
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size);

    public static void main(String[] args) {
        for (int i = 0; i < size; i++) {
            bloomFilter.put(i);
        }
        long startTime = System.nanoTime();
        if (bloomFilter.mightContain(29999)) {
            System.out.println("命中了");
        }
        long endTime = System.nanoTime();
        log.info("程序运行时间: " + (endTime - startTime) + "纳秒");
    }
}

输出:命中了
程序运行时间: 219386纳秒
也就是说,判断一个数是否属于一个百万级别的集合,只要0.219ms即可。

Guava

上面已经使用过Guava封装好的工具类。这里做一下源码分析,Guava提供好几个构造函数,最后都是调用下面这个:

static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
}

有两个比较重要的参数,expectedInsertions表示预计存放多少数据,fpp表示可以接受的误报率,Guava 通过这两个参数计算出你应当会使用的数组大小 numBits 以及需要计算几次 Hash 函数 numHashFunctions 。

long numBits = optimalNumOfBits(expectedInsertions, fpp);
int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);

应用

布隆过滤器至少有如下三个使用场景:

  1. 网页爬虫对URL的去重,避免爬取相同的URL地址
  2. 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(垃圾短信)
  3. 防止缓存击穿,将已存在的缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。

其中第三点,具体来说:IO是很多系统的瓶颈。当请求消息查询某些信息时,可能先检查缓存中是否有相关信息,有的话返回,如果没有的话可能就要去数据库里面查询。问题:如果大量请求是在请求数据库根本不存在的数据,数据库就要频繁响应这种不必要的IO查询。布隆过滤器可以派生用场。

Redis

防止缓存击穿。

Redisson客户端工具也通过一个API,org.redisson.RedissonBloomFilter,和Guava类似,tryInit方法接受2个参数,先计算得到size,再计算得到hash函数次数:

public boolean tryInit(long expectedInsertions, double falseProbability) {
    size = optimalNumOfBits(expectedInsertions, falseProbability);
    hashIterations = optimalNumOfHashFunctions(expectedInsertions, size);

    CommandBatchService executorService = new CommandBatchService(commandExecutor.getConnectionManager());
    executorService.evalReadAsync(configName, codec, RedisCommands.EVAL_VOID,
            "local size = redis.call('hget', KEYS[1], 'size');" +
                    "local hashIterations = redis.call('hget', KEYS[1], 'hashIterations');" +
                    "assert(size == false and hashIterations == false, 'Bloom filter config has been changed')",
                    Arrays.<Object>asList(configName), size, hashIterations);
    executorService.writeAsync(configName, StringCodec.INSTANCE,
                                            new RedisCommand<Void>("HMSET", new VoidReplayConvertor()), configName,
            "size", size, "hashIterations", hashIterations,
            "expectedInsertions", expectedInsertions, "falseProbability", BigDecimal.valueOf(falseProbability).toPlainString());
        executorService.execute();
    return true;
}

参考

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

johnny233

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值