关于布隆过滤器的使用

360 阅读4分钟

作用

用于判断一个元素是不是在一个集合里,实际上是一个很长的二进制向量和一系列随机映射函数

优缺点

它的优点是空间效率和查询时间远远高于一般的算法,存储空间和插入/查询时间都是常数,不需要存储元素本身,缺点是有一定的误差,随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣

原理

当一个元素被加入集合时,会进行以下操作

1、通过K个散列函数将这个元素映射成一个位数组中的K个点

2、把K个点的位置设置为1

判断一个元素是否存在集合中,会进行以下操作

1、对该元素进行相同的哈希计算

2、如果得到的值在集合中的每个位置都为1,则说明这个元素可能在过滤器中;如果这些点有任何一个0,则被检元素一定不在

布隆过滤器应用场景

  1. 判断给定数据是否存在、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)、邮箱的垃圾邮件过滤、黑名单功能。
  2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重

实现一个简单的布隆过滤器

public  class Hash {
    /**
     * 二进制向量数组的大小
     */
     private int size;
    /**
     * 随机数种子
     */
    private int seed;

    public Hash(int cap,int seed){
        this.size=cap;
        this.seed=seed;
    }

    /**
     * 计算哈希值,也就是二进制向量计算位置
     */
     public int hash(String value){
         int result=0;
         int len=value.length();
         for (int i = 0; i < len; i++) {
             result=seed*result+value.charAt(i);
         }
         return (size-1)&result;
     }
}

public class BloomFilter {
    /**
     * 二进制向量的位数,相当于能存储1000万条url左右,误报率为千万分之一
     */
    private static final int BIT_SIZE = 2 << 28;
    /**
     * 用于生成信息指纹的8个随机数,最好选取质数,hash8次生成8个位置在二进制向量
     */
    private static final int[] seeds = new int[]{3, 5, 7, 11, 13, 31, 37, 61};
    /**
     * 二进制向量大小
     */
    private BitSet bits = new BitSet(BIT_SIZE);
    /**
     * 用于存储8个随机哈希值对象
     */
    private Hash[] func = new Hash[seeds.length];

    /**
     * 构造的时候生成hash种子,对hash存储对象
     */
    public BloomFilter() {
        for (int i = 0; i < seeds.length; i++) {
            func[i] = new Hash(BIT_SIZE, seeds[i]);
        }
    }

    /**
     * 向过滤器中添加字符串
     *
     * @param value
     */
    public void addValue(String value) {
        /**
         * 将字符串value哈希为8个或多个整数,然后在这些整数的bit上变为1
         */
        if (value != null) {
            for (Hash f : func) {
                bits.set(f.hash(value), true);
            }
        }
    }

    /**
     * 判断字符串是否包含布隆过滤器中
     *
     * @param value
     * @return
     */
    public boolean contains(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        /**
         * 将要比较的字符串计算hash值,再与布隆过滤器比对
         */
        for (Hash f : func) {
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }
    public static void main(String[] args) {
        //向布隆过滤器中添加值
        BloomFilter b = new BloomFilter();
        b.addValue("jks");
        b.addValue("sdf");
        //判断是否存在
        System.out.println(b.contains("jks"));
        System.out.println(b.contains("as"));
    }

}

通过guava 实现的布隆过滤器

引入依赖

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>28.0-jre</version>
        </dependency>

测试代码

public class BloomFilterUttil {
    public static void main(String[] args) {
        int num=100000;
        /**
         * 默认误判率为3%
         */
        BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), num);

        for(int i=0;i<num;i++){
            filter.put(i);
        }
        int count=0;
        /**
         * 那不存在的数据进行测试
         */
        for (int i = num; i < num + 10000; i++) {
            if(filter.mightContain(i)){
                count++;
            }
        }
        /**
         * 测试结果(一共误判了286个,误判率:0.0286)
         */
        System.out.println("一共误判了"+count);
        System.out.println("误判率:"+(double)count/10000);
    }
}

测试结果的误判率近似3%,根据需求,可以修改误判率

通过 redisson 实现的布隆过滤器

guava 实现的布隆过滤器不错,但是它只能是单机,如果服务器重启,数据也就没了

引入pom

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>2.11.5</version>
        </dependency>

示例代码

RBloomFilter<SomeObject> bloomFilter = redisson.getBloomFilter("sample");
// 初始化布隆过滤器,预计统计元素数量为55000000,期望误差率为0.03
bloomFilter.tryInit(55000000L, 0.03);
bloomFilter.add(new SomeObject("field1Value", "field2Value"));
bloomFilter.add(new SomeObject("field5Value", "field8Value"));
bloomFilter.contains(new SomeObject("field1Value", "field8Value"));