天天看点

布隆过滤器 & Guava布隆过滤器的使用简介思想特点优点缺点图解java代码

简介

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

思想

布隆过滤器的原理:当一个元素被加入集合时,通过 K 个散列(hash)函数将这个元素映射成一个位(bit)数组中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检元素一定不在;如果都是 1,则被检元素很可能存在。

特点

某条数据一定不存在或可能存在。

优点

空间:存储数据使用的位数组,极大节省了空间。若在内存中存储海量数据,提升是很可观的;

时间:插入、查询的时间复杂度都是O(k),k为hash函数数量;

效率:哈希函数相互之间没有关系,可以在硬件指令层面并行计算;

数据加密:布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势;

缺点

准确率:“某样东西一定不存在或者可能存在”,不能保证100%的准确匹配;

无法删除数据:只能插入和查询元素,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。

在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。

图解

网上找的图:

布隆过滤器 & Guava布隆过滤器的使用简介思想特点优点缺点图解java代码
布隆过滤器 & Guava布隆过滤器的使用简介思想特点优点缺点图解java代码
布隆过滤器 & Guava布隆过滤器的使用简介思想特点优点缺点图解java代码

 图三:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。

java代码

布隆过滤器添加元素

  • 将要添加的元素给k个哈希函数
  • 得到对应于位数组上的k个位置
  • 将这k个位置设为1

布隆过滤器查询元素

  • 将要查询的元素给k个哈希函数
  • 得到对应于位数组上的k个位置
  • 如果k个位置有一个为0,则肯定不在集合中
  • 如果k个位置全部为1,则可能在集合中

使用google的Guava布隆过滤器

1.引入:

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

2.使用:

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

/**
 * 谷歌开源的Guava布隆过滤器
 * @author yangzihe
 * @date 2021/6/3
 */
public class GuavaBloomFilter {
    public static void main(String[] args) {
        // 总数量
        int total = 1000000;
        // 默认误判率fpp0.03
        BloomFilter<CharSequence> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total);
        // 初始化 total 条数据到过滤器中
        for (int i = 0; i < total; i++) {
            bf.put("" + i);
        }
        // 判断值是否存在过滤器中
        int count = 0;
        for (int i = 0; i < total + 10000; i++) {
            if (bf.mightContain("" + i)) {
                count++;
            }
        }
        System.out.println("已匹配数量 " + count);// (1000309 - 1000000)/(1000000 + 10000) * 100 ≈ 0.030594059405940593

        //指定误判率:万分之一,提高匹配精度
        BloomFilter<CharSequence> bfWithFpp = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), total, 0.0001);
        for (int i = 0; i < total; i++) {
            bfWithFpp.put("" + i);
        }
        int countFpp = 0;
        for (int i = 0; i < total + 10000; i++) {
            if (bfWithFpp.mightContain("" + i)) {
                countFpp++;
            }
        }
        //误判率 fpp 的值越小,匹配的精度越高。当减少误判率 fpp 的值,需要的存储空间也越大,所以在实际使用过程中需要在误判率和存储空间之间做个权衡。
        System.out.println("指定误判率已匹配数量 " + countFpp);// (1000001 - 1000000)/(1000000 + 10000) * 100 ≈ 0.0001
    }
}
           

布隆过滤器的误判率(FPP):

布隆过滤器 &amp; Guava布隆过滤器的使用简介思想特点优点缺点图解java代码
  • n:要添加的元素数量;
  • k:哈希的次数;
  • m:布隆过滤器的长度(如比特数组的大小);

布隆过滤器的长度 m 可以根据给定的误判率(FFP)的和期望添加的元素个数 n 的通过如下公式计算:

布隆过滤器 &amp; Guava布隆过滤器的使用简介思想特点优点缺点图解java代码

最优哈希函数数量k,如果m是数组长度,n是插入的元素个数,k是hash函数的个数,k计算公式如下:

布隆过滤器 &amp; Guava布隆过滤器的使用简介思想特点优点缺点图解java代码

Guava布隆过滤器源码记录

BloomFilter的4个成员变量:

/** bit数组:Guava实现的以CAS方式设置每个bit位 */
    private final LockFreeBitArray bits;
    /** hash函数的个数 */
    private final int numHashFunctions;
    /** guava中将对象转换为byte的通道 */
    private final Funnel<? super T> funnel;
    /** 将byte转换为n个bit的策略,也是bloomfilter hash映射的具体实现 */
    private final Strategy strategy;
           

LockFreeBitArray:封装了布隆过滤器底层bit数组的操作。

numHashFunctions:哈希函数的个数。

Funnel:它和PrimitiveSink配套使用,能将任意类型的对象转化成Java基本数据类型,默认用java.nio.ByteBuffer实现,最终均转化为byte数组。

Strategy:strategy是布隆过滤器的哈希策略,即数据如何映射到位数组,其具体方法在BloomFilterStrategies枚举中。代码如下,主要有2个方法,put和mightContain。

interface Strategy extends java.io.Serializable {
        /** 设置元素 */
        <T> boolean put(T object, Funnel<? super T> funnel, int numHashFunctions, BloomFilterStrategies.LockFreeBitArray bits);
        /** 判断元素是否存在 */
        <T> boolean mightContain(T object, Funnel<? super T> funnel, int numHashFunctions, BloomFilterStrategies.LockFreeBitArray bits);
        ...
    }
           

构造函数:

BloomFilter只有一个私有构造函数,对外提供了5个public重载的create方法,在缺省情况下误判率设定为3%,采用BloomFilterStrategies.MURMUR128_MITZ_64的实现。5个重载的create方法最终实现逻辑:

@VisibleForTesting
  static <T> BloomFilter<T> create(
      Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
    checkNotNull(funnel);
    checkArgument(
        expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
    checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
    checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
    checkNotNull(strategy);

    if (expectedInsertions == 0) {
      expectedInsertions = 1;
    }
    /*
     * TODO(user): Put a warning in the javadoc about tiny fpp values, since the resulting size
     * is proportional to -log(p), but there is not much of a point after all, e.g.
     * optimalM(1000, 0.0000000000000001) = 76680 which is less than 10kb. Who cares!
     */
    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    try {
      return new BloomFilter<T>(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
    }
  }
           

该方法接受4个参数:funnel是插入数据的Funnel,expectedInsertions是期望插入的元素总个数n,fpp即期望误判率p,strategy即哈希策略。

m:位数组的长度;

k:哈希函数的个数;

由上可知,m和k分别通过optimalNumOfBits()方法和optimalNumOfHashFunctions()方法来估计。

@VisibleForTesting
  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)));
  }

  @VisibleForTesting
  static int optimalNumOfHashFunctions(long n, long m) {
    // (m / n) * log(2), but avoid truncation due to division!
    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
  }
           
布隆过滤器 &amp; Guava布隆过滤器的使用简介思想特点优点缺点图解java代码
布隆过滤器 &amp; Guava布隆过滤器的使用简介思想特点优点缺点图解java代码

optimalNumOfHashFunctions()方法的逻辑                         optimalNumOfBits()方法的逻辑

如果指定期望误判率p,那么最优的m值与期望元素数n呈线性关系。

最优的k值实际上只与p有关,与m和n都无关,即:

布隆过滤器 &amp; Guava布隆过滤器的使用简介思想特点优点缺点图解java代码

所以,在创建BloomFilter时,确定合适的p和n值很重要。

在BloomFilterStrategies枚举中定义了两种哈希策略,都基于著名的MurmurHash算法,分别是MURMUR128_MITZ_32和MURMUR128_MITZ_64。

参考:https://www.jianshu.com/p/bef2ec1c361f

Guava 提供的布隆过滤器的实现还是很不错的,但是它有一个重大的缺陷就是只能单机使用(另外,容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了。然后,为了解决布隆过滤器无法删除数据的问题,引入了counting bloom filter计数式布隆过滤器。redis布隆、counting布隆有缘再续吧。。。