Redis布隆过滤器在Springboot中的两种实现方式(含具体工具类)

1. 背景与目标

高并发业务常见的“是否出现过/是否已处理/是否重复”判断,若用 Set 容器会带来巨大内存开销。布隆过滤器(Bloom Filter) 用很小的空间,换取“允许极少数误判(false positive),绝不漏判(no false negative)”的查询能力,尤其适合:

  • 幂等校验:是否已点赞/是否已投票/是否推送过;
  • 反爬限流:URL 去重、IP 见过即拒;
  • 缓存穿透保护:不存在的 key 快速拦截。

本文落地两种方案:

  1. Redisson 封装RBloomFilter,开箱即用、接口友好。
  2. 自实现位图:Redis SETBIT/GETBIT + MurmurHash,多语言可复用,透明可控。

2. 布隆过滤器工作原理

  • 准备一个长度为 m 的 bit 数组,初始全 0。
  • 选定 k 个独立哈希函数,对元素计算出 k 个下标,将这些位置置 1。
  • 判断元素是否存在:再次计算 k 个下标,只要有一个位置是 0 → 肯定不存在都为 1 → 认为存在(但可能是误判)。

3. 方案一:基于 Redisson 的 RBloomFilter

3.1 代码解读

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 使用 Redisson 封装的布隆过滤器工具类
 */
@Component
public class BloomFilterUtil {

    private final RedissonClient redissonClient;

    @Value("${spring.data.redis.bloom.default-expected-insertions:1000000}")
    private long defaultExpectedInsertions;

    @Value("${spring.data.redis.bloom.default-false-probability:0.001}")
    private double defaultFalseProbability;

    public BloomFilterUtil(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    /**
     * 获取或初始化 BloomFilter
     */
    private RBloomFilter<String> getFilter(String key) {
        RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(key);
        // 如果还没有初始化,调用 tryInit(不会覆盖已存在的配置)
        bloomFilter.tryInit(defaultExpectedInsertions, defaultFalseProbability);
        return bloomFilter;
    }

    /**
     * 添加元素
     */
    public boolean add(String key, String item) {
        return getFilter(key).add(item);
    }

    /**
     * 检查元素是否存在
     */
    public boolean exists(String key, String item) {
        return getFilter(key).contains(item);
    }

    /**
     * 获取当前 BloomFilter 元素个数
     */
    public long count(String key) {
        return getFilter(key).count();
    }
}


关键点
  • tryInit(n, p):幂等初始化,不会覆盖已存在配置。适合集群多实例并发启动,不易打爆。
  • add/contains:包好了多哈希与位图细节,接口简洁。
  • count():用于估算当前已加入元素数量(近似/统计意义上的计数,不是严格精确)。
适用场景
  • Redisson 已在项目中使用,追求快速稳定上线;
  • 需要一个带良好 API 的 Bloom 结构,不想维护底层哈希与位图。

3.2 工程化注意事项

  1. 初始化并发:多实例同时 tryInit 一般安全,但如果你需要调整历史过滤器的参数(比如从 100w→300w),务必:
    • 先停写新 key;
    • 删除旧 key 或切换新 key 后再 tryInit
    • 全量重放/回填(若需要保留历史)。
  2. 键名规范与分桶bf:like:{biz}:{bucket},按业务路由/时间窗分桶,避免单个过滤器无界增长。
  3. TTL 与轮换布隆过滤器不支持删除元素。针对自然老化的场景,推荐按天/周滚动 bucket,设置 TTL 防止“位污染”长期累积。
  4. 监控:暴露 count() 与 key 存活数;评估误判率趋势(误判上升说明负载逼近 n)。
  5. 集群拓扑:Redisson 适配 Redis 单机/哨兵/集群多种模式,但要确保连接与路由稳定,避免跨槽错误。

4. 方案二:Redis 位图 + MurmurHash 自实现

4.1 代码解读

@Component
public class RedisBloomFilterUtil<T> {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /** 单元素添加:逐位 SETBIT */
    public <T> void add(BloomFilterHelper<T> helper, String key, T value) {
        int[] offset = helper.murmurHashOffset(value);
        for (int i : offset) {
            redisTemplate.opsForValue().setBit(key, i, true);
        }
    }

    /** 批量添加:Pipeline 提升吞吐(单 key) */
    public void addList(BloomFilterHelper<CharSequence> helper, String key, List<String> values) {
        redisTemplate.executePipelined(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                connection.openPipeline();
                for (String v : values) {
                    int[] offset = helper.murmurHashOffset(v);
                    for (int i : offset) {
                        connection.setBit(key.getBytes(), i, true);
                    }
                }
                return null;
            }
        });
    }

    /** 存在性判断:逐位 GETBIT,出现 0 即不存在 */
    public <T> boolean contains(BloomFilterHelper<T> helper, String key, T value) {
        int[] offset = helper.murmurHashOffset(value);
        for (int i : offset) {
            if (!redisTemplate.opsForValue().getBit(key, i)) {
                return false;
            }
        }
        return true;
    }
}
关键点
  • 位图实现:完全依赖 SETBIT/GETBIT,所有语言与客户端可复用。
  • 哈希器BloomFilterHelper.murmurHashOffset(value) 计算 k 个下标。MurmurHash 无需加密性质,速度快、分布好。
  • Pipeline:批量插入显著减少 RTT;注意“单 key”批处理天然适合管道化。

4.2 工程化注意事项

  1. 集群插槽:Redis Cluster 中,同一个 key 的位操作天然在同一槽,Pipeline 能生效;不要把同一批位操作拆到多个 key。
  2. 参数变更:位图大小 m 和哈希个数 k 一旦确定,不可在线变更,需要换新 key 重新初始化。
  3. 位图扩容SETBIT 会隐式扩展字符串,但已置位的数据结构不变;贸然更换 m/k 会导致旧数据“脏位”与新参数不一致,带来不可预期误判。
  4. 持久化与内存:位图底层是 String/byte[],AOF/RDB 按字节体量持久化。评估内存与持久化压力:
    • 参考前文例子:n=1e6, p=1e-3,约 1.71MB/过滤器;
    • 每次置位是小写入;大规模回填可能触发 AOF 膨胀,建议开启 AOF 重写
  5. 并发初始化:建议用 SETNX 或分布式锁(如 Redisson RLock)保护首次初始化流程,避免多实例同时切换参数或清理旧 key。
  6. 哈希实现一致性:多语言/多服务必须保证 相同哈希算法/相同 k/相同种子,否则会出现跨服务“同一元素不同位”的严重错误。

5. 两种方案对比

维度Redisson RBloomFilter位图自实现(SETBIT/GETBIT)
易用性高:tryInit/add/contains/count 封装完善中:需实现哈希器与参数管理
语言栈Java 生态友好任何语言可实现
可控性内部实现黑盒(但够用)完全可控,可调参/自定义指标
集群适配Redisson 已适配自己处理 key 路由与 pipeline
监控count() 等接口需自建统计与监控
风险升级/兼容由 Redisson 维护维护成本在团队

选择建议

  • 快速上线、Java 单栈:优先 Redisson;
  • 多语言访问、定制化强:优先位图自实现。

6. 初始化与并发控制实践

6.1 Redisson 版本

tryInit 已尽量幂等,但参数调整时建议加启动屏障:

RLock lock = redissonClient.getLock("lock:bf:init:" + keyNamespace);
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
    try {
        RBloomFilter<String> bf = redissonClient.getBloomFilter(key);
        if (!bf.isExists()) { // 自定义元数据标记亦可
            bf.tryInit(n, p);
        }
    } finally {
        lock.unlock();
    }
}

6.2 位图版本

  • 使用 SETNX bf:{key}:meta 写入版本号/参数;成功则初始化位图并写入参数;失败说明有人先初始化过。
  • 参数变更:新建 bf:{key}:v2:*,灰度切流,旧桶 TTL 后淘汰。

7. 性能优化与批处理策略

  • 批量写入:位图方案用 executePipelined 批量 SETBIT,吞吐大幅提升。
  • 读多写少:可缓存最近一批 contains=true 的热点 key,短期内减少重复哈希。
  • 降级策略:Redis 异常时,布隆过滤器可临时放行或走备用路径(日志打点,避免业务硬阻断)。

8. 监控与容量管理

  • 指标:bloom_count(估算已加数量)、误判率抽检、GET key 大小监控、错误/超时次数。
  • 预警:当 count 接近 n 的 70%~80% 时预警;当抽检误判率超过阈值,触发换桶/扩容
  • 压测:构造随机数据,按 n 的 1.2~1.5 倍规模打入,抽样验证误判率是否低于目标 p

9. 常见问题(FAQ)

Q1:布隆过滤器能删除吗?
标准布隆过滤器不支持删除;若强需求,请考虑 Counting Bloom Filter(计数布隆,每位用小计数器,内存开销更大)。

Q2:count() 是否精确?
Redisson 的 count() 为估算值,适用于趋势监控,不用于精确结算。

Q3:多业务共用一个过滤器可行吗?
不建议。业务特征不同导致不同的 n/p 最优解,混用会互相污染;至少以业务、时间窗或前缀分桶。

Q4:误判带来哪些影响?
允许“把没见过的误判为见过”,导致少量请求被当作已处理。幂等、幂等校验、去重等场景通常可以接受;若不可接受,需改用更重的结构或调整 p

Q5:RedisBloom 模块 vs 上述两种方案?
RedisBloom 是 Redis 官方模块,内置丰富布隆族结构;如果你的环境允许加载模块或用 Redis Stack,它是第三种优选方案。但很多托管版不支持模块加载,故文中给出 Redisson 与位图两种“纯 Redis 核心指令”的实现。

10. 小结与落地清单

  • 先评估业务的 n/p,用公式估算内存与哈希次数,确认预算可接受;
  • 选型:Java 单栈→Redisson;多语言/强定制→位图;
  • 设计键名与分桶策略,配套 TTL + 轮换
  • 初始化加 并发保护,参数变更 换新桶
  • 建立 监控/预警/压测,在误判率升高前轮换。

附:两个实现的“最小可用”示例

Redisson 版

public boolean liked(String bizKey, long userId, long typeId) {
    String key = "bf:like:" + bizKey; // 如 ARTICLE
    String item = userId + ":" + typeId;
    return bloomFilterUtil.exists(key, item);
}
public void markLiked(String bizKey, long userId, long typeId) {
    String key = "bf:like:" + bizKey;
    String item = userId + ":" + typeId;
    bloomFilterUtil.add(key, item);
}

位图版(单元素)

public boolean liked(BloomFilterHelper<String> helper, String key, String item) {
    return redisBloomFilterUtil.contains(helper, key, item);
}
public void markLiked(BloomFilterHelper<String> helper, String key, String item) {
    redisBloomFilterUtil.add(helper, key, item);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值