一、Bloom Filter 的原理
布隆过滤器(Bloom Filter)是一种高效的概率型数据结构,1970 年由 Burton Howard Bloom 提出,用于快速判断一个元素是否可能存在于一个集合中。它的核心优势在于空间效率高和查询速度快,但存在一定的误判率,且不支持删除操作。
1. 数据结构
Bloom Filter 由以下两部分组成:
- 一个很长的二进制向量(bit array):初始时所有位都为 0。
- 一系列随机映射函数(hash functions):通常是 k 个独立的哈希函数,用于将元素映射到二进制向量的多个位置。
2. 工作原理
- 添加元素 :
- 对输入元素使用 k 个哈希函数,生成 k 个哈希值。
- 将这些哈希值对 bit array 的长度取模,得到 k 个索引位置。
- 将 bit array 中对应的 k 个位置置为 1。
- 查询元素 :
- 对查询元素使用相同的 k 个哈希函数,生成 k 个哈希值。
- 检查 bit array 中对应的 k 个位置是否都为 1。
- 如果有一个位置为 0,则该元素一定不在集合中。
- 如果所有位置都为 1,则该元素可能在集合中(存在误判)。
- 误判率(False Positive Rate): 误判率与 bit array 的大小(m)、哈希函数数量(k)和集合元素数量(n)密切相关。公式为: [ P = (1 - e^{-kn/m})^k ] 通过调整 m 和 k,可以在空间效率和误判率之间取得平衡。
3. 优点
- 空间效率高:相比存储完整数据集,占用内存极小。
- 查询速度快:只需计算哈希函数并检查 bit array,时间复杂度为 O(k)。
4. 不足之处
- 误判率:可能将不在集合中的元素误判为存在(False Positive),但不会漏判(False Negative)。
- 不支持删除:由于多个元素可能共享相同的 bit 位,删除一个元素可能影响其他元素。
- 无法精确计数:只能判断是否存在,无法统计元素出现次数。
二、Bloom Filter 在 Redis 中的集成
Redis 原生并不直接支持 Bloom Filter,但通过插件(如 RedisBloom)可以实现。RedisBloom 是一个独立的模块,通常需要手动编译和加载到 Redis 中。
1. 安装 RedisBloom
以下是安装 RedisBloom 的步骤(以 Linux 系统为例):
bash
# 下载 RedisBloom 源码
git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom
# 编译
make
# 将编译好的模块加载到 Redis
redis-server --loadmodule /path/to/redisbloom.so
2. RedisBloom 提供的命令
RedisBloom 提供了以下常用命令:
BF.ADD key item
:将单个元素添加到布隆过滤器。BF.MADD key item1 item2 ...
:将多个元素添加到布隆过滤器。BF.EXISTS key item
:检查元素是否存在。BF.MEXISTS key item1 item2 ...
:检查多个元素是否存在。BF.RESERVE key error_rate capacity
:创建一个指定误判率和容量的布隆过滤器。
3. 代码流程(以 Java 和 Jedis 为例)
以下是一个使用 Jedis 操作 Redis Bloom Filter 的示例:
java
import redis.clients.jedis.Jedis;
public class BloomFilterExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 创建一个布隆过滤器,误判率为 0.01,预期容量为 10000
jedis.sendCommand(() -> "BF.RESERVE".getBytes(), "mybloom", "0.01", "10000");
// 添加元素
jedis.sendCommand(() -> "BF.ADD".getBytes(), "mybloom", "apple");
jedis.sendCommand(() -> "BF.ADD".getBytes(), "mybloom", "banana");
// 检查元素是否存在
Long existsApple = (Long) jedis.sendCommand(() -> "BF.EXISTS".getBytes(), "mybloom", "apple");
Long existsOrange = (Long) jedis.sendCommand(() -> "BF.EXISTS".getBytes(), "mybloom", "orange");
System.out.println("apple exists: " + (existsApple == 1)); // true
System.out.println("orange exists: " + (existsOrange == 1)); // false
jedis.close();
}
}
4. RedisBloom 源码分析
RedisBloom 的核心实现位于 bloom.c
文件中,以下是部分关键逻辑的简化说明:
-
初始化布隆过滤器 :
cBloomFilter *bf = createBloomFilter(error_rate, capacity); bf->bits = calloc(size, sizeof(uint8_t)); // 分配 bit array bf->hashes = k; // 设置哈希函数数量
-
添加元素 :
cvoid bloomAdd(BloomFilter *bf, const char *item) { for (int i = 0; i < bf->hashes; i++) { uint64_t hash = murmurhash(item, i); // 使用 MurmurHash 计算哈希值 uint64_t idx = hash % bf->size; setBit(bf->bits, idx); // 设置对应位置为 1 } }
-
查询元素 :
cint bloomCheck(BloomFilter *bf, const char *item) { for (int i = 0; i < bf->hashes; i++) { uint64_t hash = murmurhash(item, i); uint64_t idx = hash % bf->size; if (!getBit(bf->bits, idx)) return 0; // 有一个位为 0,则不在集合中 } return 1; // 所有位均为 1,可能存在 }
完整源码可在 RedisBloom GitHub 查看。
三、为什么使用布谷鸟过滤器?
Bloom Filter 的不足(尤其是无法删除元素)限制了其在某些场景下的应用。这时,布谷鸟过滤器(Cuckoo Filter)成为一个更好的选择。
1. 布谷鸟过滤器的优势
- 支持删除:通过存储指纹(fingerprint)而非简单 bit,可以安全删除元素。
- 更高的空间效率:在低误判率下比 Bloom Filter 更节省空间。
- 依然保持高效查询:查询复杂度为 O(1)。
2. 布谷鸟过滤器原理
- 数据结构 :
- 一个由多个桶(bucket)组成的哈希表,每个桶可以存储多个指纹。
- 每个元素通过两个哈希函数映射到两个候选桶。
- 添加元素 :
- 计算两个哈希值,确定两个候选桶。
- 如果任一桶有空位,则将元素的指纹存入。
- 如果两个桶都满,则随机踢出一个已有元素("布谷鸟"策略),重新插入被踢出的元素。
- 删除元素 :
- 在两个候选桶中查找指纹。
- 如果找到匹配的指纹,直接删除。
- 查询元素 :
- 检查两个候选桶中是否存在匹配的指纹。
3. Redis 中的布谷鸟过滤器实现
RedisBloom 同样支持布谷鸟过滤器(Cuckoo Filter),命令包括:
CF.ADD key item
:添加元素。CF.DEL key item
:删除元素。CF.EXISTS key item
:检查元素是否存在。CF.RESERVE key capacity
:创建指定容量的布谷鸟过滤器。
4. 示例代码(Jedis)
java
import redis.clients.jedis.Jedis;
public class CuckooFilterExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 创建一个容量为 10000 的布谷鸟过滤器
jedis.sendCommand(() -> "CF.RESERVE".getBytes(), "mycuckoo", "10000");
// 添加元素
jedis.sendCommand(() -> "CF.ADD".getBytes(), "mycuckoo", "apple");
// 检查元素
Long existsApple = (Long) jedis.sendCommand(() -> "CF.EXISTS".getBytes(), "mycuckoo", "apple");
System.out.println("apple exists: " + (existsApple == 1)); // true
// 删除元素
jedis.sendCommand(() -> "CF.DEL".getBytes(), "mycuckoo", "apple");
// 再次检查
existsApple = (Long) jedis.sendCommand(() -> "CF.EXISTS".getBytes(), "mycuckoo", "apple");
System.out.println("apple exists after delete: " + (existsApple == 1)); // false
jedis.close();
}
}
5. RedisBloom 中布谷鸟过滤器源码
源码位于 cuckoo.c
文件中,核心逻辑如下:
-
插入元素 :
cint cuckooInsert(CuckooFilter *cf, const char *item) { uint64_t h1 = hash1(item); uint64_t h2 = hash2(item); uint8_t fingerprint = getFingerprint(item); if (insertToBucket(cf, h1, fingerprint) || insertToBucket(cf, h2, fingerprint)) { return 1; // 插入成功 } // 触发"布谷鸟"策略,递归插入 return relocate(cf, h1, h2, fingerprint); }
-
删除元素 :
cint cuckooDelete(CuckooFilter *cf, const char *item) { uint64_t h1 = hash1(item); uint64_t h2 = hash2(item); uint8_t fingerprint = getFingerprint(item); if (removeFromBucket(cf, h1, fingerprint) || removeFromBucket(cf, h2, fingerprint)) { return 1; // 删除成功 } return 0; }
四、总结与对比
- Bloom Filter :
- 适合不需要删除的场景(如缓存穿透、URL 去重)。
- 空间效率高,但误判率不可避免,且不支持删除。
- Cuckoo Filter :
- 适合需要动态添加和删除的场景(如黑名单管理)。
- 支持删除,误判率更低,但在实现上稍复杂。
在 Redis 中,二者都通过 RedisBloom 模块实现,源码开放且易于扩展。选择哪种过滤器取决于具体业务需求:若删除不是必需,Bloom Filter 更简单高效;若需要动态调整集合,Cuckoo Filter 是更好的选择。