布隆过滤器(Bloom Filter)是1970年由一个叫Bloom的老哥提出的。本质上属于一种数据结构,实际组成是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
布隆过滤器的原理:当一个元素被加入集合时,通过 K 个散列(hash)函数将这个元素映射成一个位(bit)数组中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检元素一定不在;如果都是 1,则被检元素很可能存在。
某条数据一定不存在或可能存在。
空间:存储数据使用的位数组,极大节省了空间。若在内存中存储海量数据,提升是很可观的;
时间:插入、查询的时间复杂度都是O(k),k为hash函数数量;
效率:哈希函数相互之间没有关系,可以在硬件指令层面并行计算;
数据加密:布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势;
准确率:“某样东西一定不存在或者可能存在”,不能保证100%的准确匹配;
无法删除数据:只能插入和查询元素,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。
网上找的图:
图三:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。
布隆过滤器添加元素
布隆过滤器查询元素
1.引入:
com.google.guava
guava
30.1.1-jre
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 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 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):
布隆过滤器的长度 m 可以根据给定的误判率(FFP)的和期望添加的元素个数 n 的通过如下公式计算:
最优哈希函数数量k,如果m是数组长度,n是插入的元素个数,k是hash函数的个数,k计算公式如下:
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 {
/** 设置元素 */
boolean put(T object, Funnel super T> funnel, int numHashFunctions, BloomFilterStrategies.LockFreeBitArray bits);
/** 判断元素是否存在 */
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 BloomFilter 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(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)));
}
optimalNumOfHashFunctions()方法的逻辑 optimalNumOfBits()方法的逻辑
如果指定期望误判率p,那么最优的m值与期望元素数n呈线性关系。
最优的k值实际上只与p有关,与m和n都无关,即:
所以,在创建BloomFilter时,确定合适的p和n值很重要。
在BloomFilterStrategies枚举中定义了两种哈希策略,都基于著名的MurmurHash算法,分别是MURMUR128_MITZ_32和MURMUR128_MITZ_64。
参考:https://www.jianshu.com/p/bef2ec1c361f
Guava 提供的布隆过滤器的实现还是很不错的,但是它有一个重大的缺陷就是只能单机使用(另外,容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了。然后,为了解决布隆过滤器无法删除数据的问题,引入了counting bloom filter计数式布隆过滤器。redis布隆、counting布隆有缘再续吧。。。