引言
布隆过滤器(Bloom Filter)由 Burton Howard Bloom 于1970年提出,是一种空间效率极高的概率型数据结构。它能够以极小的内存开销判断一个元素”可能存在”或”肯定不存在”于集合中,允许少量误判(false positive)但绝不会漏判(false negative)。Google Chrome 的恶意网址检测、HBase 和 Cassandra 的 SSTable 查询优化、比特币网络的交易过滤都广泛使用了布隆过滤器。
本文将用 Java 实现一个标准布隆过滤器,深入讲解位数组布局、多哈希函数设计、误判率计算与优化策略。
核心原理
布隆过滤器由以下两部分组成:
- 位数组(Bit Array):长度为 m 的数组,所有位初始化为 0
- k 个独立哈希函数:每个函数将元素映射到位数组的一个位置
插入流程
对元素 x 执行 k 次哈希,得到 k 个位置索引,将这 k 个位置都置为 1。
查询流程
对元素 x 执行同样的 k 次哈希,检查这 k 个位置是否都为 1:
– 如果任意一位为 0,则 x 肯定不在集合中
– 如果全为 1,则 x 可能在集合中(存在误判可能)
误判率公式
当插入 n 个元素后,某一位仍为 0 的概率为:
$$p = \left(1 – \frac{1}{m}\right)^{kn} \approx e^{-kn/m}$$
误判率(所有 k 个位置恰好都为 1 的概率):
$$\epsilon \approx \left(1 – e^{-kn/m}\right)^k$$
最优哈希函数数量(使误判率最小):
$$k = \frac{m}{n} \ln 2$$
此时最优位数组大小:
$$m = -\frac{n \ln \epsilon}{(\ln 2)^2}$$
Java 实现
基础版布隆过滤器
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.BitSet;
public class BloomFilter<T> {
private final BitSet bitSet;
private final int bitSize; // 位数组大小 m
private final int hashCount; // 哈希函数数量 k
private final int seed; // 哈希种子,用于生成不同的哈希函数
private int elementCount; // 已插入元素数量
/**
* 创建布隆过滤器
* @param expectedElements 预计插入元素数量 n
* @param falsePositiveRate 期望误判率 ε(如 0.01 表示 1%)
*/
public BloomFilter(int expectedElements, double falsePositiveRate) {
// 根据公式计算最优位数组大小 m
this.bitSize = (int) Math.ceil(
-expectedElements * Math.log(falsePositiveRate) / (Math.log(2) * Math.log(2))
);
// 计算最优哈希函数数量 k
this.hashCount = (int) Math.ceil(
(double) bitSize / expectedElements * Math.log(2)
);
this.bitSet = new BitSet(bitSize);
this.seed = 0x5bd1e995; // MurmurHash 常用种子
this.elementCount = 0;
System.out.println("布隆过滤器配置: m=" + bitSize + ", k=" + hashCount);
}
/**
* MurmurHash 变体:将对象转换为多个哈希值
* 通过不同的偏移量模拟 k 个独立哈希函数
*/
private int[] generateHashes(T element) {
int[] hashes = new int[hashCount];
byte[] data = element.toString().getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < hashCount; i++) {
hashes[i] = murmurHash(data, seed + i * 0x9e3779b9) % bitSize;
if (hashes[i] < 0) {
hashes[i] += bitSize;
}
}
return hashes;
}
/**
* 简化的 MurmurHash3 32-bit 实现
*/
private int murmurHash(byte[] data, int seed) {
int hash = seed;
final int c1 = 0xcc9e2d51;
final int c2 = 0x1b873593;
final int r1 = 15;
final int r2 = 13;
final int m = 5;
final int n = 0xe6546b64;
int length = data.length;
int offset = 0;
// 每次处理 4 个字节
while (length >= 4) {
int k = (data[offset] & 0xff) |
((data[offset + 1] & 0xff) << 8) |
((data[offset + 2] & 0xff) << 16) |
((data[offset + 3] & 0xff) << 24);
k *= c1;
k = (k << r1) | (k >>> (32 - r1));
k *= c2;
hash ^= k;
hash = (hash << r2) | (hash >>> (32 - r2));
hash = hash * m + n;
offset += 4;
length -= 4;
}
// 处理剩余字节
int k = 0;
switch (length) {
case 3: k ^= (data[offset + 2] & 0xff) << 16;
case 2: k ^= (data[offset + 1] & 0xff) << 8;
case 1: k ^= (data[offset] & 0xff);
k *= c1;
k = (k << r1) | (k >>> (32 - r1));
k *= c2;
hash ^= k;
}
hash ^= data.length;
hash ^= (hash >>> 16);
hash *= 0x85ebca6b;
hash ^= (hash >>> 13);
hash *= 0xc2b2ae35;
hash ^= (hash >>> 16);
return hash;
}
/**
* 向过滤器中添加元素
*/
public void add(T element) {
int[] hashes = generateHashes(element);
for (int hash : hashes) {
bitSet.set(hash);
}
elementCount++;
}
/**
* 判断元素是否可能在集合中
* @return true: 可能存在(可能误判);false: 肯定不存在
*/
public boolean mightContain(T element) {
int[] hashes = generateHashes(element);
for (int hash : hashes) {
if (!bitSet.get(hash)) {
return false; // 有一位为 0,肯定不存在
}
}
return true; // 全为 1,可能存在
}
/**
* 计算当前实际误判率的理论值
*/
public double getCurrentFalsePositiveRate() {
return Math.pow(1 - Math.exp(-hashCount * (double) elementCount / bitSize), hashCount);
}
/**
* 获取位数组填充率
*/
public double getFillRate() {
return (double) bitSet.cardinality() / bitSize;
}
public int getBitSize() {
return bitSize;
}
public int getHashCount() {
return hashCount;
}
public int size() {
return elementCount;
}
/**
* 清空过滤器
*/
public void clear() {
bitSet.clear();
elementCount = 0;
}
}
计数布隆过滤器(支持删除)
标准布隆过滤器不支持删除,因为删除可能误删其他元素对应的位置。计数布隆过滤器将每一位扩展为计数器:
public class CountingBloomFilter<T> {
private final short[] counters; // 使用 short 作为计数器
private final int bitSize;
private final int hashCount;
private final int seed;
private int elementCount;
public CountingBloomFilter(int expectedElements, double falsePositiveRate) {
this.bitSize = (int) Math.ceil(
-expectedElements * Math.log(falsePositiveRate) / (Math.log(2) * Math.log(2))
);
this.hashCount = (int) Math.ceil(
(double) bitSize / expectedElements * Math.log(2)
);
this.counters = new short[bitSize];
this.seed = 0x5bd1e995;
this.elementCount = 0;
}
// 复用相同的哈希生成逻辑...
private int[] generateHashes(T element) {
int[] hashes = new int[hashCount];
byte[] data = element.toString().getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < hashCount; i++) {
hashes[i] = Math.abs(murmurHash(data, seed + i * 0x9e3779b9)) % bitSize;
}
return hashes;
}
private int murmurHash(byte[] data, int seed) {
// 简化版 MurmurHash,与上文相同
int hash = seed;
for (int i = 0; i < data.length; i++) {
hash ^= (data[i] & 0xff);
hash *= 0x01000193;
}
return hash;
}
public void add(T element) {
int[] hashes = generateHashes(element);
for (int hash : hashes) {
if (counters[hash] < Short.MAX_VALUE) {
counters[hash]++;
}
}
elementCount++;
}
public void remove(T element) {
int[] hashes = generateHashes(element);
for (int hash : hashes) {
if (counters[hash] > 0) {
counters[hash]--;
}
}
elementCount--;
}
public boolean mightContain(T element) {
int[] hashes = generateHashes(element);
for (int hash : hashes) {
if (counters[hash] <= 0) {
return false;
}
}
return true;
}
}
性能测试与验证
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
public class BloomFilterDemo {
public static void main(String[] args) {
int expectedElements = 100_000;
double targetFPR = 0.01; // 1% 目标误判率
BloomFilter<String> filter = new BloomFilter<>(expectedElements, targetFPR);
Set<String> actualSet = new HashSet<>();
Random random = new Random();
System.out.println("=== 布隆过滤器测试 ===\n");
// 插入 10 万个元素
System.out.println("插入 " + expectedElements + " 个元素...");
for (int i = 0; i < expectedElements; i++) {
String element = "user-" + random.nextInt(10_000_000);
filter.add(element);
actualSet.add(element);
}
System.out.println("位数组大小: " + filter.getBitSize() + " bits = " +
(filter.getBitSize() / 8 / 1024) + " KB");
System.out.println("哈希函数数: " + filter.getHashCount());
System.out.println("位数组填充率: " + String.format("%.2f%%", filter.getFillRate() * 100));
System.out.println("理论误判率: " + String.format("%.4f%%", filter.getCurrentFalsePositiveRate() * 100));
System.out.println("HashSet 占用内存估算: " + (expectedElements * 32 / 1024) + " KB+\n");
// 测试:查询已插入的元素(应全部命中)
int truePositives = 0;
for (String element : actualSet) {
if (filter.mightContain(element)) {
truePositives++;
}
}
System.out.println("已插入元素查询命中率: " + truePositives + "/" + actualSet.size());
// 测试:查询未插入的元素(统计误判)
int falsePositives = 0;
int testCount = 100_000;
for (int i = 0; i < testCount; i++) {
String element = "test-" + random.nextInt(10_000_000) + "-new";
if (!actualSet.contains(element) && filter.mightContain(element)) {
falsePositives++;
}
}
System.out.println("未插入元素误判数: " + falsePositives + "/" + testCount);
System.out.println("实际误判率: " + String.format("%.4f%%", (double) falsePositives / testCount * 100));
}
}
布隆过滤器的变体
| 变体 | 特性 | 适用场景 |
|---|---|---|
| 标准布隆过滤器 | 不支持删除,空间最小 | URL 去重、缓存穿透防护 |
| 计数布隆过滤器 | 支持删除,空间 x4 | 动态集合、元素可移除 |
| 分层布隆过滤器 | 支持滑动窗口过期 | 流式数据、时间窗口过滤 |
| 可扩展布隆过滤器 | 动态扩容 | 元素数量不确定 |
| 布谷鸟过滤器 | 支持删除,更低误判率 | 需要删除功能的场景 |
实际应用场景
- 数据库查询优化:HBase/Cassandra 用布隆过滤器避免读取不存在的 SSTable
- 缓存穿透防护:Redis 前部署布隆过滤器,拦截肯定不存在的 key
- URL 去重:爬虫系统快速判断 URL 是否已抓取
- 比特币 SPV 节点:过滤不相关的交易,减少带宽
- 推荐系统去重:防止重复推荐用户已看过的内容
总结
布隆过滤器以可控的误判率为代价,换取了极高的空间效率(通常比哈希表节省 90% 以上内存)。其核心设计思想——”用概率换空间”——在大数据和高并发场景下具有不可替代的价值。掌握布隆过滤器的原理与实现,是构建高性能系统的必备技能。