Appearance
Redis BloomFilter
本文介绍Bloom Filter(布隆过滤器)的原理、实现以及使用。
1. 介绍与原理
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。
布隆过滤器存在误判,有可能将不存在集合中的元素判断为存在集合中。
布隆过滤器实质上是一个很长的二进制位数组(bit array)和一系列随机映射函数(哈希函数)。
布隆过滤器原理如下:
- 初始化: 创建一个长度为 m 的位数组,所有位都初始化为0。同时,选择 k 个不同的哈希函数。
- 添加元素: 当需要将一个元素添加到集合中时,会用这 k 个哈希函数分别对该元素进行计算,得到 k 个哈希值。这些哈希值会映射到位数组中的 k 个位置,然后,将这 k 个位置上的位都设置为1。
- 查询元素: 当需要判断一个元素是否在集合中时,同样用这 k 个哈希函数对该元素进行计算,得到 k 个哈希值,并检查位数组中对应的 k 个位置。
- 如果这 k 个位置中有任何一个位是0,那么该元素一定不在集合中。 (因为如果它在集合中,在添加的时候这些位都应该被设置为1了)
- 如果这 k 个位置上的位都是1,那么该元素“可能”在集合中。 这就是布隆过滤器的“概率性”所在,因为有可能这些位是在添加其他元素时被设置为1的,这被称为“误判”(False Positive)或“假阳性”。
2. 自定义布隆过滤器
要实现布隆过滤器,即实现初始化、添加元素和查询元素这三个功能:
初始化:最主要的任务是确定位数组长度m和哈希函数个数k,当然,我们可以直接在构造方法中指定这两个值,但是也可以通过计算得出。一般定义布隆过滤器时,会考虑以下两个参数:
- 预计存储的元素数量 (n): 大概会往布隆过滤器中添加多少个元素。
- 可接受的误判率 (P): 能容忍的假阳性概率是多少。这个值越小,所需的空间和哈希函数数量就越大。
基于n和P,可通过以下公式计算位数组长度和哈希函数个数:
- 位数组长度 (m) 的计算公式:$ m=− \frac{n \times lnP}{(ln2)^{2}}$
- 哈希函数数量 (k) 的计算公式:
,即
添加元素:对该元素进行k次哈希计算,得到k个哈希值,将这k个哈希值映射到位数组的k个位置,将这k个位置上的位设置为1;
查询元素:按照添加元素的方法得到k个哈希值,判断这k个哈希值映射到位数组的k个位置,是否存在其中一个位置为0,如果是,则返回
false,如果全部位置都是1,则返回true(无法判断该元素是真的存在集合中还是误判);
首先引入Guava依赖库(用于引入哈希函数):
是一个由 Google 开发的开源 Java 核心库集合。它提供了一系列实用的工具类和新的数据结构,旨在简化 Java 开发,提高代码质量和开发效率。
可以把 Guava 理解为一个 Java 开发的“瑞士军刀”,它包含了许多在日常编程中非常有用但 JDK 标准库中缺失或不方便使用的功能。
xml
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.4.8-jre</version>
</dependency>自定义过滤器如下:
java
public class MyBloomFilter {
private BitSet bitSet;
private int length;
private int numberOfHash;
/**
* 创建布隆过滤器
* @param number 预计插入的元素数量
* @param fpp 误判率,0-1之间
*/
public MyBloomFilter(int number, double fpp){
int m = (int) (-Math.log(fpp) * number / (Math.log(2) * Math.log(2)));
length = m;
numberOfHash = (int)(-Math.log(fpp) / Math.log(2));
bitSet= new BitSet(m);
}
/**
* 向布隆过滤器中添加元素
* @param element 元素
*/
public void add(int element){
for (int hashPosition : getHashPositions(element)) {
bitSet.set(hashPosition % length);
}
}
private int[] getHashPositions(int element){
int[] positions = new int[numberOfHash];
for (int i = 0; i < numberOfHash; i++) {
positions[i] = hash(element, i);
}
return positions;
}
private int hash(int element, int position){
HashCode hashCode = Hashing.murmur3_128(0).hashInt(element);
HashCode hashCode1 = Hashing.murmur3_128(1).hashInt(element);
return Math.abs(hashCode.asInt() + position * hashCode1.asInt());
}
/**
* 判断布隆过滤器中是否包含element元素
* @param element
* @return
*/
public boolean contain(int element){
for (int hashPosition : getHashPositions(element)) {
int p = hashPosition % length;
if (!bitSet.get(p)) {
return false;
}
}
return true;
}
}测试代码:
java
public static void main(String[] args) {
MyBloomFilter bloomFilter = new MyBloomFilter(10000, 0.03);
for (int i = 0; i < 10000; i++) {
bloomFilter.add(i);
}
int falsePositiveNumber = 0;
for (int i = 10000; i < 11000; i++) {
if(bloomFilter.contain(i)) {
System.out.println("误判了:" + i);
falsePositiveNumber++;
}
}
System.out.println("总共误判了:" + falsePositiveNumber);
}结果如下:
txt
误判了:10109
误判了:10133
误判了:10135
误判了:10271
误判了:10276
误判了:10306
误判了:10351
误判了:10433
误判了:10454
误判了:10462
误判了:10464
误判了:10563
误判了:10586
误判了:10665
误判了:10730
误判了:10821
误判了:10859
误判了:10880
误判了:10884
误判了:10944
误判了:10945
误判了:10990
总共误判了:223. Guava版布隆过滤器
在Guava中也提供了布隆过滤器实现,我们可以直接使用。
使用如下静态方法创建Guava的布隆过滤器:
java
public static <T extends @Nullable Object> BloomFilter<T> create(
Funnel<? super T> funnel, long expectedInsertions, double fpp){
// ...
}Funnel<? super T> funnel:布隆过滤器的核心是哈希函数,这些哈希函数操作的是原始的字节序列。然而,在 Java 中,我们通常处理的是各种复杂的对象(例如字符串、整数、自定义的User对象、Product对象等),布隆过滤器不知道User等对象应该如何转换为字节序列。Funnel就是为了解决这个问题而设计的。它是一个泛型接口Funnel<T>,其中T是想要放入布隆过滤器中的对象的类型。javapublic interface Funnel<T extends @Nullable Object> extends Serializable { void funnel(@ParametricNullness T from, PrimitiveSink into); }当实现
Funnel时,需要提供一个funnel方法,该方法负责:- 接收自定义对象
T。 - 接收一个
PrimitiveSink对象(原始类型的数据槽)。 - 将
T对象的关键属性(那些希望参与哈希计算的属性)“写入”到PrimitiveSink中。
对于常见的 Java 类型,Guava 提供了
Funnels工具类,其中包含了许多预定义的Funnel实现,非常方便:Funnels.stringFunnel(Charset charset):用于字符串。Funnels.byteArrayFunnel():用于字节数组。Funnels.integerFunnel():用于整型。Funnels.longFunnel():用于长整型。
- 接收自定义对象
long expectedInsertions:布隆过滤器中期望放入元素的数量;double fpp:期望的误判率;
使用案例如下:
java
void test1(){
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000, 0.01);
for (int i = 0; i < 10000; i++) {
bloomFilter.put(i);
}
int falsePositiveNumber = 0;
for (int i = 10000; i < 11000; i++) {
if(bloomFilter.mightContain(i)){
System.out.println("误判了:" + i);
falsePositiveNumber++;
}
}
System.out.println("总共误判了:" + falsePositiveNumber);
}结果如下:
txt
误判了:10067
误判了:10071
误判了:10143
误判了:10233
误判了:10303
误判了:10370
误判了:10666
误判了:10843
误判了:10889
误判了:10893
误判了:10906
误判了:10937
总共误判了:124. Redis版布隆过滤器
Redis也提供了布隆过滤器的实现,使用如下。
创建布隆过滤器:
txt
BF.RESERVE key error_rate capacitykey:布隆过滤器名称;error_rate:误判率,0-1之间,capacity:布隆过滤器容量,即期望插入的元素数量;
向布隆过滤器中添加元素:
txt
BF.ADD key item判断布隆过滤器中是否存在某个元素:
txt
BF.EXISTS key item返回值为1表示可能存在,返回值为0表示不存在。
使用案例如下:

注意,Redis 8之后自带Redis Bloom模块,如果是之前的版本,需要自行加载模块。
5. 布隆过滤器的应用
缓存穿透: 解决缓存穿透问题,避免大量对不存在数据的请求直接打到数据库,造成数据库压力。例如,在查询缓存前,先用布隆过滤器判断key是否存在,如果不存在则直接返回,避免查询缓存和数据库。
垃圾邮件过滤: 将已知的垃圾邮件地址或垃圾邮件特征添加到布隆过滤器中,快速识别并过滤掉垃圾邮件。
黑名单/白名单过滤: 用于网站或应用的用户黑名单、敏感词过滤等。
大数据去重: 在海量数据中进行去重操作,例如爬虫抓取URL去重,推荐系统过滤已推荐内容等。
推荐系统: 过滤掉用户已经看过或不感兴趣的内容,避免重复推荐。