布隆过滤器(Bloom Filter)
一、简述
Bloom Filter(布隆过滤器)是1970年由Burton Howard Bloom提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和不支持删除。
BloomFilter能解决什么问题?以少量的内存空间判断一个元素是否属于这个集合,代价是有一定的错误率
二、应用场景
布隆过滤器的应用场景围绕“海量数据快速去重/判断”展开,以下是典型场景及原理:
2.1 网络与缓存场景
- URL去重(Web爬虫):爬虫需避免重复爬取同一URL,用布隆过滤器存储已爬URL,判断新URL是否已处理(数据量级可达数十亿,传统存储无法承受)。
- HTTP缓存加速:缓存服务器(如Nginx)用布隆过滤器存储已缓存的URL,收到请求时先判断URL 是否在缓存中,命中则直接返回,避免回源拉取。
2.2 数据过滤场景
垃圾邮件过滤:邮件服务器用布隆过滤器存储“垃圾邮件发送者的 IP/域名黑名单”,收到邮件时快速判断发送方是否在黑名单中,减少后续校验成本。
数据库防穿透:在缓存(如Redis)与数据库之间加布隆过滤器,存储“数据库中已存在的主键”,请求查询不存在的主键时,直接返回空,避免穿透到数据库。
2.3 分布式系统场景
- HBase随机读优化:HBase在列族(CF)级别配置布隆过滤器,存储“StoreFile中的行键/列键”,随机读(Get)时先通过布隆过滤器排除不包含目标键的StoreFile,减少IO操作。
- P2P网络资源查找:P2P节点用布隆过滤器存储“本地拥有的资源索引”,其他节点查询资源时,先通过布隆过滤器判断该节点是否有目标资源,避免无效通信。
三、工作原理
Bloom Filter的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k

Bloom Filter的一个例子集合S{x,y,z}。带有颜色的箭头表示元素经过k(k=3)hash函数的到在M(bit数组)中的位置。元素W不在S集合中,因为元素W经过k个hash函数得到在M(bit数组)的k个位置中存在值为0的位置。
-
初始化一个数组,所有位标为
0,A={x1,x2,x3,…,xm} (x1,x2,x3,…,xm初始为0) -
将已知集合
S中的每一个数组,按以下方式映射到A中2.0 选取
n个互相独立的hash函数h1,h2,…hk
2.1 将元素通过以上hash函数得到一组索引值h1(xi),h2(xi),…,hk(xi)
2.2 将集合A中的上述索引值标记为1(如果不同元素有重复,则重复覆盖为1,这是一个觅等操作) -
对于一个元素
x,将其根据2.0中选取的hash函数,进行hash,得到一组索引值h1(x),h2(x),…,hk(x)如果集合A中的这些索引位置上的值都是1,表示这个元素属于集合S,否则则不属于S
3.1 前提
hash函数的计算不能性能太差,否则得不偿失- 任意两个
hash函数之间必须是独立的.
即任意两个hash函数不存在单一相关性,否则hash到其中一个索引上的元素也必定会hash到另一个相关的索引上,这样多个hash没有意义
3.2 错误率
工作原理的第3步得出来的结论,一个是绝对靠谱的,一个是不能100%靠谱的。在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。关于具体的错误率,这和最优的哈希函数个数以及位数组的大小有关,而这是可以估算求得一个最优解的:哈希函数个数k、位数组大小m及字符串数量n之间存在相互关系。相关文献证明了对于给定的m、n,当\(k = ln(2)* m/n\)时出错的概率是最小的。
具体的请看:Bloom Filter概念和原理
3.3 基本特征
从以上对基本原理和数学基础的分析,我们可以得到Bloom filter的如下基本特征,用于指导实际应用。
- 存在一定错误率,发生在正向判断上(存在性),反向判断不会发生错误(不存在性);
- 错误率是可控制的,通过改变位数组大小、
hash函数个数或更低碰撞率的hash函数来调节; - 保持较低的错误率,位数组空位至少保持在一半以上;
- 给定
m和n,可以确定最优hash个数,即k = ln2 * (m/n),此时错误率最小; - 给定允许的错误率
E,可以确定合适的位数组大小,即m >= log2(e) * (n * log2(1/E)),继而确定hash函数个数k; - 正向错误率无法完全消除,即使不对位数组大小和
hash函数个数进行限制,即无法实现零错误率; - 空间效率高,仅保存“存在状态”,但无法存储完整信息,需要其他数据结构辅助存储;
- 不支持元素删除操作,因为不能保证删除的安全性。
四、实现
4.1 手动实现
知道了布隆过滤器的原理之后就可以自己手动实现一个,步骤如下
- 一个合适大小的位数组保存数据
- 几个不同的哈希函数
- 添加元素到位数组(布隆过滤器)的方法实现
- 判断给定元素是否存在于位数组(布隆过滤器)的方法实现。
import java.util.BitSet;
public class MyBloomFilter {
//位数组的大小
private static final int DEFAULT_SIZE = 2 << 24;
//通过这个数组可以创建 6 个不同的哈希函数
private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};
//位数组。数组中的元素只能是 0 或者 1
private BitSet bits = new BitSet(DEFAULT_SIZE);
//存放包含 hash 函数的类的数组
private SimpleHash[] func = new SimpleHash[SEEDS.length];
//初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
public MyBloomFilter() {
// 初始化多个不同的 Hash 函数
for (int i = 0; i < SEEDS.length; i++) {
func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
}
}
//添加元素到位数组
public void add(Object value) {
for (SimpleHash f : func) {
bits.set(f.hash(value), true);
}
}
//判断指定元素是否存在于位数组
public boolean contains(Object value) {
boolean ret = true;
for (SimpleHash f : func) {
ret = ret && bits.get(f.hash(value));
}
return ret;
}
//静态内部类。用于 hash 操作!
public static class SimpleHash {
private int cap;
private int seed;
public SimpleHash(int cap, int seed) {
this.cap = cap;
this.seed = seed;
}
//计算 hash 值
public int hash(Object value) {
int h;
return (value == null)
? 0
: Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
}
}
}
4.2 guava的实现
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class GuavaBloomFilter {
public static void main(String[] args) {
//创建布隆过滤器,设置存储的数据类型,预期数据量,误判率(必须大于0,小于1)
BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 100, 0.01);
//存入元素
bloomFilter.put("test");
//判断元素是否存在
System.out.println(bloomFilter.mightContain("test"));
}
}
在以上示例中,当mightContain()方法返回true时,我们可以99%确定该元素在过滤器中,当过滤器返回false时,我们可以100%确定该元素不存在于过滤器中。
Guava提供的布隆过滤器的实现还是很不错的,但是有一个重大的缺陷就是只能单机使用(容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到Redis中的布隆过滤器了。
4.3 redis的实现
4.3.1 介绍
Redis v4.0之后有了Module(模块/插件)功能,Redis Modules让Redis可以使用外部模块扩展其功能。布隆过滤器就是其中的Module。详情可以查看Redis官方对Redis Modules的介绍:https://redis.io/modules
另外,官网推荐了一个RedisBloom作为Redis布隆过滤器的Module地址:https://github.com/RedisBloom/RedisBloom其他还有:
- redis-lua-scaling-bloom-filter(lua脚本实现):https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter
- pyreBloom(Python中的快速Redis布隆过滤器):https://github.com/seomoz/pyreBloom
- ......
RedisBloom提供了多种语言的客户端支持,包括:Python、Java、JavaScript和PHP。
4.3.2 使用Docker安装
具体地址:https://hub.docker.com/r/redislabs/rebloom/
具体操作如下:
➜ ~ docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
➜ ~ docker exec -it redis-redisbloom bash
root@21396d02c252:/data# redis-cli
127.0.0.1:6379> Copy to clipboardErrorCopied
4.3.3 常用命令一览
注意:
key:布隆过滤器的名称,item:添加的元素。
BF.ADD:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:BF.ADD {key} {item}。BF.MADD:将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式BF.ADD与之相同,只不过它允许多个输入并返回多个值。格式:BF.MADD {key} {item} [item ...]。BF.EXISTS:确定元素是否在布隆过滤器中存在。格式:BF.EXISTS {key} {item}。BF.MEXISTS:确定一个或者多个元素是否在布隆过滤器中存在格式:BF.MEXISTS {key} {item} [item ...]。
另外,BF.RESERVE命令需要单独介绍一下:
这个命令的格式如下:
BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]。
下面简单介绍一下每个参数的具体含义:
key:布隆过滤器的名称error_rate:误报的期望概率。这应该是介于0到1之间的十进制值。例如,对于期望的误报率0.1%(1000中为1),error_rate应该设置为0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU使用率越高。capacity:过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。
可选参数:
- expansion:如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以
expansion。默认扩展值为2。这意味着每个后续子过滤器将是前一个子过滤器的两倍。
4.3.4 实际使用
127.0.0.1:6379> BF.ADD myFilter java
(integer) 1
127.0.0.1:6379> BF.ADD myFilter javax
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter java
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter javax
(integer) 1
127.0.0.1:6379> BF.EXISTS myFilter github
(integer) 0

浙公网安备 33010602011771号