1. 背景与目标
高并发业务常见的“是否出现过/是否已处理/是否重复”判断,若用 Set 容器会带来巨大内存开销。布隆过滤器(Bloom Filter) 用很小的空间,换取“允许极少数误判(false positive),绝不漏判(no false negative)”的查询能力,尤其适合:
- 幂等校验:是否已点赞/是否已投票/是否推送过;
- 反爬限流:URL 去重、IP 见过即拒;
- 缓存穿透保护:不存在的 key 快速拦截。
本文落地两种方案:
- Redisson 封装:
RBloomFilter,开箱即用、接口友好。 - 自实现位图: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 工程化注意事项
- 初始化并发:多实例同时
tryInit一般安全,但如果你需要调整历史过滤器的参数(比如从 100w→300w),务必:- 先停写新 key;
- 删除旧 key 或切换新 key 后再
tryInit; - 全量重放/回填(若需要保留历史)。
- 键名规范与分桶:
bf:like:{biz}:{bucket},按业务路由/时间窗分桶,避免单个过滤器无界增长。 - TTL 与轮换:布隆过滤器不支持删除元素。针对自然老化的场景,推荐按天/周滚动 bucket,设置 TTL 防止“位污染”长期累积。
- 监控:暴露
count()与 key 存活数;评估误判率趋势(误判上升说明负载逼近 n)。 - 集群拓扑: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 工程化注意事项
- 集群插槽:Redis Cluster 中,同一个 key 的位操作天然在同一槽,Pipeline 能生效;不要把同一批位操作拆到多个 key。
- 参数变更:位图大小
m和哈希个数k一旦确定,不可在线变更,需要换新 key 重新初始化。 - 位图扩容:
SETBIT会隐式扩展字符串,但已置位的数据结构不变;贸然更换m/k会导致旧数据“脏位”与新参数不一致,带来不可预期误判。 - 持久化与内存:位图底层是
String/byte[],AOF/RDB 按字节体量持久化。评估内存与持久化压力:- 参考前文例子:
n=1e6, p=1e-3,约 1.71MB/过滤器; - 每次置位是小写入;大规模回填可能触发 AOF 膨胀,建议开启 AOF 重写。
- 参考前文例子:
- 并发初始化:建议用
SETNX或分布式锁(如 RedissonRLock)保护首次初始化流程,避免多实例同时切换参数或清理旧 key。 - 哈希实现一致性:多语言/多服务必须保证 相同哈希算法/相同 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);
}
2万+

被折叠的 条评论
为什么被折叠?



