布隆过滤器Bloom filter


前言

在处理缓存穿透问题时,常常会提到使用布隆过滤器,将所有有效的key记录进布隆过滤器,之后查询缓存前先通过布隆过滤器校验,可以阻挡大部分不存在的key,本文就介绍一下布隆过滤器的一些使用方法和大致的实现原理。


一、什么是布隆过滤器?

布隆过滤器(Bloom filter)是一种节省空间的概率数据结构,由 Burton Howard Bloom 于 1970 年构思,用于测试元素是否是集合的成员。假阳性匹配是可能的,但不会假阴性, 换句话说,查询结果在集合中的有可能不存在,不在集合中的一定不在集合中。
元素可以添加到集合中,但不能删除,添加的项目越多,误报的概率就越大。

二、使用Guava BloomFilter步骤

1.引入依赖

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

2.创建BloomFilter实例

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.StandardCharsets;

BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 1000);

3.元素添加和检测

bloomFilter.put("key1");
bloomFilter.put("key2");
bloomFilter.mightContain("key1"); // true
bloomFilter.mightContain("key3"); // false 

二、Guava BloomFilter实现

1. 添加元素

首先,调用strategy的put方法,参数包括 :
object 添加的对象
funnel 用于对象和byte转换
numHashFunctions 哈希次数,根据创建BloomFilter时期望的总元素数和错误率使用公式计算得到
bits 用于存放数据的二进制位,总位数同样根据创建BloomFilter时期望的总元素数和错误率使用公式计算得到

  @CanIgnoreReturnValue
  public boolean put(T object) {
    return strategy.put(object, funnel, numHashFunctions, bits);
  }

strategy 是 com.google.common.hash.BloomFilter.Strategy 接口,其实现类为 BloomFilterStrategies,使用枚举实例有两个实现,分别为 BloomFilterStrategies.MURMUR128_MITZ_32BloomFilterStrategies.MURMUR128_MITZ_64,这两个就是布隆过滤器的核心实现。

strategy 默认为 MURMUR128_MITZ_64,接下来看下其实现:

MURMUR128_MITZ_64() {
    @Override
    public <T> boolean put(
        T object, Funnel<? super T> funnel, int numHashFunctions, LockFreeBitArray bits) {
      long bitSize = bits.bitSize();
      // 使用murmur3_128 哈希算法对对象进行hash,哈希结果为128位
      byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal(); 
      long hash1 = lowerEight(bytes); // 取低位8字节64位,转为long
      long hash2 = upperEight(bytes); // 取高位8字节64位,转为long

      boolean bitsChanged = false;
      long combinedHash = hash1; // 合计hash,初始值为低位64位
      for (int i = 0; i < numHashFunctions; i++) {
        // 使用 & Long.MAX_VALUE 来对 负数 或者 += 操作后long溢出 转为正式
        bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
        combinedHash += hash2;
      }
      return bitsChanged;
    }

    private long lowerEight(byte[] bytes) {
      return Longs.fromBytes(
          bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]);
    }

    private long upperEight(byte[] bytes) {
      return Longs.fromBytes(
          bytes[15], bytes[14], bytes[13], bytes[12], bytes[11], bytes[10], bytes[9], bytes[8]);
    }
  };

每个元素存储hash会计算 numHashFunctions 个位标志

2. 检测元素包含

哈希的方式和put时一致,先根据 murmur3_128 计算128位哈希,取高低64位,进行多轮的加等计算出标志位的index,去检测index上的二进制位是否都为1(true),有一位不为1 (true)则说明这个元素之前并未存入布隆过滤器

MURMUR128_MITZ_64() {
    @Override
    public <T> boolean mightContain(
        T object, Funnel<? super T> funnel, int numHashFunctions, LockFreeBitArray bits) {
      long bitSize = bits.bitSize();
      byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
      long hash1 = lowerEight(bytes);
      long hash2 = upperEight(bytes);

      long combinedHash = hash1;
      for (int i = 0; i < numHashFunctions; i++) {
        // 有一位不为true,则结果为不包含
        if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) {
          return false;
        }
        combinedHash += hash2;
      }
      return true;
    }
  };

3. 核心参数计算

总比特数的计算:

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

numHashFunctions 的计算:

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

变量定义如下:
m: 总比特数
n: 期望插入的元素数
b: m/n, 每个插入的比特数
p: 期望的假阳性概率

散列函数的数目k必须是正整数。撇开这个约束不谈,对于给定的m和n,使误报概率最小的k值是:
n m ln ⁡ 2 (1) \frac{n}{m}\ln_{}{2} \tag{1} mnln2(1)
给定n(插入元素的数量)和期望的误报概率p(并假设使用了k的最佳值),可通过替换上述概率表达式中的k的最佳值来计算所需的位数m和期望的误报概率ε:
p = ( 1 − e − ( n m ln ⁡ 2 ) n m ) ( n m ln ⁡ 2 ) (2) p = (1-e^{-(\frac{n}{m}\ln_{}{2})\frac{n}{m}} )^{(\frac{n}{m}\ln_{}{2})} \tag{2} p=(1e(mnln2)mn)(mnln2)(2)
可以简化为:
ln ⁡ p = − m n ( ln ⁡ 2 ) 2 (3) \ln{p} = - \frac{m}{n} (\ln_{}{2})^2 \tag{3} lnp=nm(ln2)2(3)
因此,每个元素的最佳位数是:
m n = − log ⁡ 2 p ln ⁡ 2 ≈ − 1.44 log ⁡ 2 p (4) \frac{m}{n} = -\frac{\log_{2}{p}}{\ln 2} \approx -1.44 \log_{2}{p} \tag{4} nm=ln2log2p1.44log2p(4)
使用相应数量的哈希函数K(忽略积分):
k = − ln ⁡ p ln ⁡ 2 = − log ⁡ 2 p (5) k = -\frac{\ln p}{\ln 2} = - \log_{2}{p} \tag{5} k=ln2lnp=log2p(5)
这意味着对于给定的误报概率p,布隆过滤器的长度m与被过滤元素的数量n成比例,并且所需的散列函数数量仅取决于目标误报概率p。
通过公式4和公式5可以得到:
k = m n ln ⁡ 2 (6) k={\frac {m}{n}}\ln 2 \tag{6} k=nmln2(6)

我们可以画一下上述公式2的图,在m=64时如下:

log_2(m)=8
可以看到当过滤元素的数量(横坐标n)越大,使用64位比特的误报概率(纵坐标p)越高,取向与1也就是全误报。

不同m值情况下的函数图如下:
在这里插入图片描述
然后 Guava 中BloomFilter 默认的误报率为0.03 也就是 3%,在这个默认值情况下m与n的关系如下:
在这里插入图片描述
m约等于7.2984*n,滤元素的数量40000个时,需要291937比特位,约35KB来存储。


总结

以上就是今天要讲的内容,本文仅仅简单介绍了Guava中的布隆过滤器的使用,而Guava提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值