Redis 布隆过滤器快速实现

Redis 布隆过滤器快速实现

一、什么是布隆过滤器

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。

核心特性:

  • 判断为"不存在",则一定不存在(100% 准确)
  • 判断为"存在",则可能存在(有一定误判率)
  • 插入的元素越多,误判率越高
  • 不存储元素本身,只占用固定大小的位数组

典型场景:

场景 说明
缓存穿透防护 查询不存在的数据时直接拦截,不穿透到 DB
爬虫 URL 去重 数十亿 URL 去重,内存只需几 GB
黑名单/白名单 无需存储完整数据,快速判断是否命中
邮件/用户名判重 注册时判断用户名是否已被使用
推荐系统去重 已推荐过的不再重复推荐

二、布隆过滤器原理

2.1 数据结构

复制代码
位数组(bit array):    ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
                         │ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ 1 │ 0 │ 0 │ 1 │ ...
                         └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
bit 索引:                 0   1   2   3   4   5   6   7   8   9

2.2 添加元素

复制代码
添加 "hello" 时:
  hash1("hello") = 2  ──► 索引 2 置 1
  hash2("hello") = 5  ──► 索引 5 置 1
  hash3("hello") = 9  ──► 索引 9 置 1

添加 "world" 时:
  hash1("world") = 1  ──► 索引 1 置 1
  hash2("world") = 4  ──► 索引 4 置 1
  hash3("world") = 8  ──► 索引 8 置 1

结果位数组:
              ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
              │ 0 │ 1 │ 1 │ 0 │ 1 │ 1 │ 0 │ 0 │ 1 │ 1 │
              └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
index:         0   1   2   3   4   5   6   7   8   9

2.3 查询元素

  • 查询 "hello" → hash 得 2,5,9 → 全是 1 → 可能存在
  • 查询 "hello1" → hash 得 2,3,7 → 位 3 是 0 → 一定不存在
  • 查询 "hello2" → hash 得 1,4,9 → 全是 1 → 可能存在(但从未添加过,这就是误判)

2.4 误判率计算

复制代码
误判率 p ≈ (1 - e^(-k*n/m))^k
  m = 位数组长度(bit 数)
  n = 插入元素数量
  k = 哈希函数个数

最优哈希函数个数:k ≈ (m/n) * ln(2) ≈ 0.7 * (m/n)

示例:m=10亿(128MB), n=100万,k≈7,误判率 p ≈ 千万分之一


三、方式一:Redis Stack / RedisBloom 模块(推荐)

Redis 7.x 已集成 RedisBloom,无需额外安装。旧版本安装 Redis Stack 即可。

3.1 创建布隆过滤器

bash 复制代码
# BF.RESERVE <key> <error_rate> <capacity> [EXPANSION expansion] [NONSCALING]
BF.RESERVE user_filter 0.01 1000000
参数 说明
key 过滤器名称
error_rate 期望误判率,越小越占内存(0~1)
capacity 预计存储的元素数量
EXPANSION 容量超限后自动扩容倍数(默认 2)
NONSCALING 禁止自动扩容

3.2 添加元素

bash 复制代码
BF.ADD user_filter "user_123"          # 单个添加,返回 1 新增 / 0 可能重复
BF.MADD user_filter "a" "b" "c"        # 批量添加 → [1, 1, 1]

3.3 查询元素

bash 复制代码
BF.EXISTS user_filter "user_123"       # 单个查询,返回 1 可能存在 / 0 一定不存在
BF.MEXISTS user_filter "a" "x" "c"     # 批量查询 → [1, 0, 1]

3.4 查看信息

bash 复制代码
BF.INFO user_filter    # 返回容量、已插入数量、子过滤器数量等
BF.CARD user_filter    # 返回独立 item 数(去重估计值)

3.5 多语言代码示例

Java(Jedis)
java 复制代码
import redis.clients.jedis.UnifiedJedis;

public class BloomFilterDemo {
    private static final String FILTER_KEY = "cache:bloom";

    public static void main(String[] args) {
        try (UnifiedJedis jedis = new UnifiedJedis("redis://localhost:6379")) {
            jedis.bfReserve(FILTER_KEY, 0.01, 1_000_000);

            jedis.bfAdd(FILTER_KEY, "article_001");
            jedis.bfAdd(FILTER_KEY, "article_002");

            String id = "article_999";
            if (!jedis.bfExists(FILTER_KEY, id)) {
                System.out.println(id + " 一定不存在,直接返回 null");
                return;
            }
            // 可能存在,查缓存或 DB
            String cache = jedis.get("article:" + id);
            if (cache != null) return;
            // 查 DB ...
        }
    }
}
Go(go-redis)
go 复制代码
import "github.com/redis/go-redis/v9"

rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
rdb.BFReserve(ctx, "myfilter", 0.01, 1000000)
rdb.BFAdd(ctx, "myfilter", "item1")
exists, _ := rdb.BFExists(ctx, "myfilter", "item1").Result()
Python(redis-py)
python 复制代码
import redis
r = redis.Redis(host='localhost', port=6379)
r.bf().reserve('myfilter', 0.01, 1000000)
r.bf().add('myfilter', 'item1')
r.bf().exists('myfilter', 'item1')   # True/False

四、方式二:Bitmap + Lua 脚本(纯 Redis,无插件)

当无法使用 Redis Stack 时,用 Redis 原生的 Bitmap(SETBIT/GETBIT)配合 Lua 手动实现。

4.1 Lua 添加元素脚本

lua 复制代码
-- bloom_add.lua,KEYS[1]=过滤器 key,ARGV=要添加的元素
local key = KEYS[1]
local bits = 1 << 31

for i = 1, #ARGV do
    local val = ARGV[i]
    local h1 = math.abs(redis.call('HASH', val) % bits)
    local h2 = math.abs(redis.call('CRC16', val) % bits)
    local h3 = math.abs((h1 + h2) % bits)
    redis.call('SETBIT', key, h1, 1)
    redis.call('SETBIT', key, h2, 1)
    redis.call('SETBIT', key, h3, 1)
end
return 1

4.2 Lua 查询元素脚本

lua 复制代码
-- bloom_exists.lua,KEYS[1]=过滤器 key,ARGV=要查询的元素,返回数组 [1,0,1,...]
local key = KEYS[1]
local bits = 1 << 31
local results = {}

for i = 1, #ARGV do
    local val = ARGV[i]
    local h1 = math.abs(redis.call('HASH', val) % bits)
    local h2 = math.abs(redis.call('CRC16', val) % bits)
    local h3 = math.abs((h1 + h2) % bits)
    if redis.call('GETBIT', key, h1) == 0
        or redis.call('GETBIT', key, h2) == 0
        or redis.call('GETBIT', key, h3) == 0 then
        results[i] = 0
    else
        results[i] = 1
    end
end
return results

4.3 Java 调用示例

java 复制代码
public class RedisBitmapBloomFilter {

    private static final String ADD_SCRIPT = loadScript("bloom_add.lua");
    private static final String EXISTS_SCRIPT = loadScript("bloom_exists.lua");

    private final JedisPool pool;
    private final String addSha;
    private final String existsSha;

    public RedisBitmapBloomFilter(JedisPool pool) {
        this.pool = pool;
        try (Jedis jedis = pool.getResource()) {
            this.addSha = jedis.scriptLoad(ADD_SCRIPT);
            this.existsSha = jedis.scriptLoad(EXISTS_SCRIPT);
        }
    }

    public void add(String key, String... values) {
        try (Jedis jedis = pool.getResource()) {
            jedis.evalsha(addSha, 1, key, values);
        }
    }

    public boolean exists(String key, String value) {
        try (Jedis jedis = pool.getResource()) {
            List<Long> result = (List<Long>) jedis.evalsha(
                existsSha, 1, key, value);
            return result != null && result.get(0) == 1;
        }
    }
}

4.4 位数组大小与内存对照

位数组大小 (bit) 内存占用 适用数据量 (1%误判)
2^28 (2.68 亿) 32 MB ~100 万
2^30 (10.7 亿) 128 MB ~400 万
2^32 (42.9 亿) 512 MB ~1600 万

五、实战:缓存穿透防护

5.1 问题

恶意攻击者用大量不存在的 ID 查询 → 每次缓存未命中 → 全部落到数据库 → DB 崩溃

5.2 方案架构

复制代码
请求 → 布隆过滤器 → 不存在 → 直接返回 null(拦截 ✓)
                  → 可能存在 → 查缓存 → 命中 → 返回
                                      → 未命中 → 查 DB → 回写缓存

5.3 Spring Boot 集成

java 复制代码
@Configuration
public class BloomFilterConfig {

    @Bean
    public UnifiedJedis unifiedJedis() {
        return new UnifiedJedis("redis://localhost:6379");
    }

    @PostConstruct
    public void initBloomFilter(UnifiedJedis jedis, ProductMapper mapper) {
        jedis.bfReserve("product:bloom", 0.01, 1_000_000);
        // 预热:将已有数据 ID 全部加入过滤器
        mapper.getAllIds().forEach(id -> jedis.bfAdd("product:bloom", id));
    }
}
java 复制代码
@Service
public class ProductService {

    private final UnifiedJedis jedis;
    private final ProductMapper mapper;

    public Product getById(String id) {
        // 1. 布隆过滤器拦截
        if (!jedis.bfExists("product:bloom", id)) {
            return null;
        }
        // 2. 缓存查询
        String cacheKey = "product:" + id;
        String cached = jedis.get(cacheKey);
        if (cached != null) return JSON.parseObject(cached, Product.class);
        // 3. 数据库查询
        Product p = mapper.selectById(id);
        if (p != null) jedis.setex(cacheKey, 3600, JSON.toJSONString(p));
        return p;
    }

    public void add(Product p) {
        mapper.insert(p);
        jedis.bfAdd("product:bloom", String.valueOf(p.getId()));
    }
}

六、常见问题

Q1:布隆过滤器可以删除元素吗?

标准布隆过滤器不支持删除。需删除时可使用 RedisBloom 的计数布隆过滤器(Cuckoo Filter):

bash 复制代码
CF.RESERVE cfilter 0.01 1000000
CF.ADD cfilter "item1"      # 添加
CF.DEL cfilter "item1"      # 删除(支持!)
CF.EXISTS cfilter "item1"   # 判断

Q2:如何选择误判率?

场景 建议误判率
缓存穿透防护 1%
URL / 爬虫去重 0.1% ~ 1%
黑名单 0.01%

Q3:布隆过滤器满了怎么办?

策略 说明
自动扩容(默认) 容量超限自动创建子过滤器,查询时遍历全部
分层过滤 按时间分片:bloom:2026-01bloom:2026-02
手动重建 监控误判率,达阈值后重建更大的过滤器

七、方案对比总结

方案 优点 缺点 适用场景
RedisBloom 模块 API 简洁、高性能、自动扩容 需 Redis Stack / 7.x 生产环境首选
Bitmap + Lua 纯 Redis,零依赖 实现复杂、不可扩容 无法装模块的过渡方案
本地 Guava 单机零网络延迟 多实例间不一致 单机应用
Redisson 封装好的分布式实现 底层仍是 Bitmap 不想手写 Lua 的场景

结论:优先用 RedisBloom;装不了再用 Bitmap + Lua 兜底。

相关推荐
深念Y3 小时前
stable-diffusion.cpp 的 FLUX.2 Klein 9B 分步
java·前端·数据库
whn19773 小时前
oracle 的ora-19633处理
数据库·oracle
小弥儿3 小时前
Firecrawl:把整个网页变成 AI 可查询的数据库
数据库·人工智能·学习
MC皮蛋侠客3 小时前
TDengine C++ 系列(8):流式计算与最新值缓存——库内实时处理
c++·缓存·tdengine
swordbob4 小时前
mysql数据库,明明有索引为啥不用
数据库·mysql
晴天¥4 小时前
Oracle ASM、ACFS、FLEX ASM三者之间的关系?
数据库·oracle
Leon-Ning Liu4 小时前
Oracle GoldenGate Replicat Abend:Key Column Missing 根因分析与修复
数据库·oracle
IvorySQL14 小时前
PostgreSQL 日报 | JOIN 与外键优化(8 月 9 日)
数据库·postgresql
ltl14 小时前
自治数据库十年回顾:Peloton、NoisePage、OtterTune 到云原生 auto-tuning
数据库