布隆过滤器的原理与设计实现

前言

对于缓存穿透问题,布隆过滤器就是一种很好的解决方案。缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,失去了缓存的意义。简而言之就是缓存中不存在,然后到数据库查询也不存在。当然,也可以设置黑白名单、分布式锁等其它方式解决,此处只讲述布隆过滤器,其它方案友友们可以自行研究。

概念

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

原理

首先布隆过滤器是一个bit数组,如图所示:
res

例如我们把华为这个信息映射到布隆过滤器中,使用三个不同的哈希函数映射到对应的bit数组的位置上(假设计算出来的位置为3,5,7),使其映射到的位置上的0变为1,如图所示:
res

再将苹果的信息也按照相同的方式映射到bit数组上(假设计算出来的位置为1,5,6),如图所示:
res

很显然,华为信息映射到的5的位置被苹果信息映射到的5的位置替换了,那么就明显可以说明如果布隆过滤器上没有某个信息,那么经过哈希处理后bit数组位置上的值应该全部为0,如果为1,该信息有可能存在也有可能不存在,就像上面的苹果信息计算出来的位置把原有位置的信息覆盖掉了。因此布隆过滤器上有映射,但实际数据也不一定存在。

布隆过滤器参数关系

根据上面的情况而言,很显然,长度过小的布隆过滤器很快所有的bit位都被置为1了,查询到的任意值都会返回“可能存在”的,这样的话那么布隆过滤器就失去了意义。说明,布隆过滤器的长度越小,其误报率就越高,布隆过滤器的长度越长,误报率越低。
对于不当的哈希函数的个数也会对对误报率有影响。例如哈希函数的个数越多,数组的bit位会被迅速填满,会加快布隆过滤器bit位置为1的速度,那么布隆过滤器的效率就会越低。简单来说,如果哈希函数的个数越多,布隆过滤器bit位置为1的速度就越快,且效率就会越低;如果哈希函数个数越少,bit位置为1的速度就越慢,但是误报率就会明显变高了。因此,布隆过滤器的长度、哈希函数的个数、误报率以及插入元素的个数这几者之间存在关系。该关系如下所示,在这里小编不对公式做过多的赘述,有兴趣的学者可以自行参考下面详情。

  • 设bitarray大小为m,样本数量为n,失误率为p
  • 单个样本大小不影响布隆过滤器大小,因为样本会通过哈希函数得到输出值
  • 使用样本数量n和失误率p可以算出m,公式为:
    res
  • 所使用哈希函数个数k可以由以下公式求得:
    res
  • 可以通过以下公式算出设计的布隆过滤器的真实失误率:
    res
    公式推导如下:
    res
    res
    res
    res
    res
    具体可以点->这里<-参考

优缺点分析

优点

  • 相比于传统的List、Set、Map等数据结构,它占用空间更少,因为其本身并不存储任何数据
  • 时间复杂度低,增加和查询元素的时间复杂为O(N),(N为哈希函数的个数,通常情况比较小)
  • 保密性强,布隆过滤器不存储元素本身

缺点

  • 其返回的结果是概率性(存在误差)的
  • 很难实现删除操作
  • 无法获取元素本身

使用场景

1、网页爬虫对URL的去重,避免爬去相同的URL地址
2、垃圾邮件过滤,从数十亿个垃圾邮件列表中判断某邮箱是否是垃圾邮箱
3、解决数据库缓存击穿,黑客攻击服务器时,会构建大量不存在于缓存中的key向服务器发起请求,在数据量足够大的时候,频繁的数据库查询会导致挂机
4、秒杀系统,查看用户是否重复购买

编码实现

有很多业界的牛人大咖已经实现了布隆过滤器,例如谷歌的guava以及hutool都已经进行了实现,此处小编仅仅用简单的java方式编码实现,也方便大家更容易理解,并没有做过多的繁琐处理。

package com.ssy.www.mode;

import java.util.BitSet;
import java.util.Random;

/**
 * @author ssy
 * @className BloomFilter
 * @description 简单布隆过滤器的实现
 * @date 2023-11-18 09:56:31
 */
public class BloomFilter {


    private BitSet bitmap;

    private Integer bitmapSize;

    private Integer hashFunctionCount;

    private Integer insertElementCount;

    private Random random = new Random();

    public BloomFilter(Integer bitmapSize, Integer insertElementCount){
        if(bitmapSize<0){
            throw new IllegalArgumentException("布隆过滤器长度不能小于0");
        }
        if(insertElementCount<0){
            throw new IllegalArgumentException("插入的元素个数不能小于0");
        }
        if(bitmapSize <= insertElementCount){
            throw new IllegalArgumentException("布隆过滤器长度必须大于插入的元素个数");
        }
        this.bitmapSize = bitmapSize;
        this.insertElementCount = insertElementCount;
        hashFunctionCount = (int) Math.round(bitmapSize/insertElementCount * Math.log(2.0));
        this.bitmap = new BitSet(this.bitmapSize);
    }

    public void add(Object obj){
        if(obj==null){
            return;
        } else {
            String str = obj.toString();
            for (Integer i = 0; i < hashFunctionCount; i++) {
                long hash = hash(str, i);
                int index = getIndex(hash);
                bitmap.set(index,Boolean.TRUE);
            }
        }
    }

    public boolean contains(Object obj){
        if(obj==null) return false;
        String str = obj.toString();
        for (Integer i = 0; i < hashFunctionCount; i++) {
            long hash = hash(str, i);
            int index = getIndex(hash);
            if (!bitmap.get(index)) {
                return false;
            }
        }
        return true;
    }

    private long hash(String element, int seed){
        random.setSeed(seed);
        long hash = 0x520abc1314defL;
        byte[] bytes = element.getBytes();
        for (int i = 0; i < bytes.length; i++) {
            hash ^= random.nextInt();
            hash *= 0x8023aa2157ffL;
            hash ^= bytes[i];
        }
        return hash;
    }

    private int getIndex(long hash){
        int index = Math.abs( (int)(hash % bitmapSize));
        return index;
    }

}

当然,上述的代码亦可以通过设置泛型,为后续指定计算具体类型的bit位置,小编在此仅做简单演示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值