RedisBloom插件:让你的Redis支持布隆和布谷鸟!缓存穿透-URL映射(不可删)/黑名单管理(可增删)


一、Bloom Filter 的原理

布隆过滤器(Bloom Filter)是一种高效的概率型数据结构,1970 年由 Burton Howard Bloom 提出,用于快速判断一个元素是否可能存在于一个集合中。它的核心优势在于空间效率高和查询速度快,但存在一定的误判率,且不支持删除操作。

1. 数据结构

Bloom Filter 由以下两部分组成:

  • 一个很长的二进制向量(bit array):初始时所有位都为 0。
  • 一系列随机映射函数(hash functions):通常是 k 个独立的哈希函数,用于将元素映射到二进制向量的多个位置。

2. 工作原理

  • 添加元素
    1. 对输入元素使用 k 个哈希函数,生成 k 个哈希值。
    2. 将这些哈希值对 bit array 的长度取模,得到 k 个索引位置。
    3. 将 bit array 中对应的 k 个位置置为 1。
  • 查询元素
    1. 对查询元素使用相同的 k 个哈希函数,生成 k 个哈希值。
    2. 检查 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 文件中,以下是部分关键逻辑的简化说明:

  • 初始化布隆过滤器

    c 复制代码
    BloomFilter *bf = createBloomFilter(error_rate, capacity);
    bf->bits = calloc(size, sizeof(uint8_t));  // 分配 bit array
    bf->hashes = k;  // 设置哈希函数数量
  • 添加元素

    c 复制代码
    void 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
        }
    }
  • 查询元素

    c 复制代码
    int 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)组成的哈希表,每个桶可以存储多个指纹。
    • 每个元素通过两个哈希函数映射到两个候选桶。
  • 添加元素
    1. 计算两个哈希值,确定两个候选桶。
    2. 如果任一桶有空位,则将元素的指纹存入。
    3. 如果两个桶都满,则随机踢出一个已有元素("布谷鸟"策略),重新插入被踢出的元素。
  • 删除元素
    1. 在两个候选桶中查找指纹。
    2. 如果找到匹配的指纹,直接删除。
  • 查询元素
    1. 检查两个候选桶中是否存在匹配的指纹。

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 文件中,核心逻辑如下:

  • 插入元素

    c 复制代码
    int 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);
    }
  • 删除元素

    c 复制代码
    int 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 是更好的选择。

相关推荐
mitt_4 分钟前
go语言变量
开发语言·后端·golang
无限大620 分钟前
二维数组搜索:从暴力地毯到二分神技的奇幻之旅
后端
bobz9651 小时前
最近玩了好多把 LOL
后端
爱欲无极1 小时前
基于Flask的微博话题多标签情感分析系统设计
后端·python·flask
cwkiller1 小时前
heapdump深度利用之信息泄露篇
后端
Olrookie4 小时前
若依前后端分离版学习笔记(五)——Spring Boot简介与Spring Security
笔记·后端·学习·spring·ruoyi
小白的代码日记4 小时前
基于 Spring Boot 的小区人脸识别与出入记录管理系统实现
java·spring boot·后端
Chaney不会代码4 小时前
Java7/8中的HashMap深挖
后端
新程快咖员4 小时前
兄弟们,你们安装IDEA 2025.2了吗?java编辑器代码提示失效?临时解决方案新鲜出炉!
后端·intellij idea
调试人生的显微镜5 小时前
移动端网页调试实战,跨设备兼容与触控交互问题排查全流程
后端