文章目录
前言
在处理缓存穿透问题时,常常会提到使用布隆过滤器,将所有有效的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_32
和 BloomFilterStrategies.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=(1−e−(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=−ln2log2p≈−1.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时如下:
可以看到当过滤元素的数量(横坐标n)越大,使用64位比特的误报概率(纵坐标p)越高,取向与1也就是全误报。
不同m值情况下的函数图如下:
然后 Guava 中BloomFilter 默认的误报率为0.03 也就是 3%,在这个默认值情况下m与n的关系如下:
m约等于7.2984*n,滤元素的数量40000个时,需要291937比特位,约35KB来存储。
总结
以上就是今天要讲的内容,本文仅仅简单介绍了Guava中的布隆过滤器的使用,而Guava提供了大量能使我们快速便捷地处理数据的函数和方法。