拓展阅读
todo
工具包包的封装+文档
资源可视化
前后端===》项目
弱点解决===》场景解决方案/技术咨询
chat
详细介绍一下 BloomFliter
好的,我们来系统、详细地介绍一下 Bloom Filter(布隆过滤器)。
一、Bloom Filter 是什么?
Bloom Filter 是一种空间效率极高的“概率型数据结构”,用于判断一个元素是否在一个集合中。
它的特点是:
- 可以判断“可能存在”或“一定不存在”
- 存在一定误判率(可能把不存在的元素误判为存在)
- 不会漏判(存在的元素一定判断为存在)
- 不支持删除元素(标准版)
它特别适合大数据量、对存储和速度要求高,但容忍一定误差的场景。
二、Bloom Filter 的核心原理
1. 数据结构组成
Bloom Filter 本质上由两部分组成:
- 一个 bit 数组(初始全为 0)
- 一组哈希函数(k 个)
2. 添加元素(以字符串 "hello"
为例)
- 用 k 个哈希函数分别对
"hello"
进行哈希,得到 k 个下标。 - 在 bit 数组中,将这 k 个下标位置置为 1。
3. 查询元素(比如查询 "world"
)
- 用相同的 k 个哈希函数对
"world"
哈希,得到 k 个下标。 -
检查这些下标在 bit 数组中对应的位置是否都是 1:
- 如果有任意一个是 0,说明一定 没出现过。
- 如果全部是 1,说明 可能出现过(但也可能是其他元素共同“碰巧”设置的)。
三、例子:可视化理解
假设我们有一个 bit 数组长度为 10:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
我们有 3 个哈希函数,向 Bloom Filter 中加入 "cat"
:
hash1("cat") % 10 = 1
hash2("cat") % 10 = 4
hash3("cat") % 10 = 7
将这些位置设为 1:
[0, 1, 0, 0, 1, 0, 0, 1, 0, 0]
查询 "dog"
时:
- 如果某一个位置是 0,直接返回“不存在”
- 如果三个位置都是 1,那就说“可能存在”
四、优缺点总结
✅ 优点
- 节省空间:比哈希表或集合更节省内存
- 查询速度快:时间复杂度为 O(k),k 是哈希函数个数
- 适合并发场景:因为只涉及 bit 操作,且不支持删除,线程安全性好
❌ 缺点
- 有误判率:查询可能返回 false positive
- 不支持删除(除非使用 Counting Bloom Filter)
- 哈希函数选择很关键:影响误判率和性能
- 无法获取元素本身的集合内容:只能判断是否存在
五、实际应用场景
🔍 1. 网页黑名单过滤
判断某个 URL 是否在黑名单中,节省内存,误判也能容忍。
📚 2. 搜索引擎的缓存过滤器
判断查询请求是否在缓存中,避免频繁查数据库或分布式系统。
🛒 3. 电商防重复推荐
防止给用户推荐已经看过或买过的商品。
📬 4. 邮件反垃圾系统
快速判断邮件地址是否在垃圾地址库中。
🧠 5. 区块链
比如 Bitcoin 中用于过滤区块数据,客户端只接受可能相关的交易。
六、扩展变种
名称 | 特点 |
---|---|
Counting Bloom Filter | 支持删除功能,用计数器代替 bit 位 |
Scalable Bloom Filter | 自动扩容以适应无限数据增长,维持误判率 |
Compressed Bloom Filter | 更节省空间,适合传输 |
Partitioned Bloom Filter | 将 bit 数组分为 k 段,每个哈希函数只对应一段,减少冲突 |
七、误判率计算(了解)
设:
- m:bit 数组大小
- k:哈希函数个数
- n:插入元素个数
则查询某个元素的误判率大约为:
\[\left(1 - e^{-kn/m} \right)^k\]你可以根据这个公式,选择合理的 m 和 k,来控制误判率。
八、与 HashSet 的比较
功能/特点 | Bloom Filter | HashSet |
---|---|---|
占用内存 | 少得多 | 多 |
查询速度 | 快 | 快 |
支持删除 | 不支持(标准版) | 支持 |
可枚举内容 | 否 | 是 |
是否存在误判 | 有 | 无 |
是否线程安全 | 较容易实现 | 需要加锁或并发容器 |
如果你正在做类似缓存、反垃圾、推荐系统或大规模日志去重等场景,Bloom Filter 是一个非常实用的工具。
需要的话,我可以给你写一份 Java 或 Python 的 Bloom Filter 实现。
如何解决数据被删除的问题呢?比如定期重新构建一下?业界比较推荐的做法是?
这是个很实际的问题,也是 Bloom Filter 在生产环境中经常需要面对的挑战:如何解决“不能删除元素”的问题?
一、背景:为什么 Bloom Filter 不支持删除?
因为 Bloom Filter 使用的是一个 bit 数组,当你添加元素时,多个元素可能会影响同一位。
比如:
- 元素 A 和元素 B 都将第 5 位设为了 1
- 如果你删除 A,把第 5 位清零了,B 的存在也被破坏了
这就是“误删”问题。
二、常见解决方案
✅ 方案一:使用 Counting Bloom Filter(计数型布隆过滤器)
原理:
把 bit 数组变成 计数数组(比如每个位置是一个 uint8
,初始为 0)
- 插入元素时,对应位置 +1
- 删除元素时,对应位置 -1
- 查询时,只要对应位置 > 0 就认为“可能存在”
优点:
- 完整支持删除操作
- 实现简单
缺点:
- 占用空间比普通 Bloom Filter 大(从 1bit 变成 4bit 或 8bit)
- 删除操作仍然不能解决“误判”问题(误删别人的数据)
👉 使用场景:比较适合缓存、实时数据流、有生命周期的数据场景。
✅ 方案二:定期“重建” Bloom Filter(Rebuild)
原理:
- Bloom Filter 本身不支持删除
- 但你可以在后台维护一份真实的数据(比如 Redis、数据库)
- 然后定期全量重新构建 Bloom Filter,清除脏数据
常见操作时机:
- 每隔一段时间,比如 30 分钟、1 小时
- 或者数据量达到某个阈值时触发
- 或者后台触发器检测到某个 key 需要删除时,打个标记等待清理
优点:
- 实现简单,兼容原始 Bloom Filter
- 不增加 Bloom Filter 的体积
缺点:
- 需要额外维护真实数据源
- 重建 Bloom Filter 期间有可能有“脏读”
👉 使用场景:大规模缓存穿透拦截(如 Redis 缓存层)
✅ 方案三:分片 + 版本化 Bloom Filter(Double Buffer / Rotating Bloom Filter)
原理:
- 同时维护多个布隆过滤器,如:FilterA 和 FilterB
- 当前写入 FilterA,同时老的 FilterB 还在使用
- 等 FilterB 过期后丢弃,轮换成新的 FilterA(类似“热数据-冷数据”)
你可以把这个模型想象成一个“定时轮换”的轮子。
优点:
- 不影响读写
- 支持定时清除旧数据
缺点:
- 需要管理多份 Bloom Filter
- 可能短期内会存在一定冗余
👉 使用场景:搜索引擎反爬、日志系统、消息去重
✅ 方案四:使用 LRU + Bloom Filter 组合方案
原理:
- Bloom Filter 做第一层判断
- 后面再跟一个 LRU 缓存记录真正查询/访问过的 key
- 当 LRU 淘汰某个 key 时,同时记录下来,延迟触发 Bloom Filter 的 rebuild
👉 使用场景:高并发系统的缓存命中判断(比如 CDN、热点视频缓存)
三、业界推荐实践(总结)
场景 | 推荐方案 | 理由 |
---|---|---|
缓存穿透拦截 | 普通 Bloom Filter + 定期重建 | 简单高效,误判影响小 |
短期数据去重 | 计数 Bloom Filter | 支持删除,去重精度高 |
大规模高并发去重 | 分片 Bloom Filter + 热备 | 低冲突,读写分离 |
需极高精度 | 用哈希集合 + LFU 或 LRU 替代 Bloom Filter | 不容忍误判时使用 |
四、小贴士:什么时候不应该用 Bloom Filter?
- 对误判极度敏感(如金融风控)
- 元素量小,不差这点空间(直接用 Set 更简单)
- 需要频繁删除和修改(会增加实现成本)
如果你给我具体场景(比如你在自动化测试平台里使用 Bloom Filter 的用途),我可以帮你选最合适的方案并提供具体实现建议。是否考虑加入 Redis、Guava、RoaringBitmap 等工具库?也可以配套分析一下。
是否可以通过 bloom-filter 作为第一层,redis 之类的缓存作为第二层,mysql 兜底的结合方案?来解决误判的问题
非常棒的想法!你说的这个组合方案,实际上已经是**业界最经典、最成熟的“缓存穿透防护三段式架构”**之一了👇
✅ 一、架构概览:三层过滤机制
请求 Key
↓
╭────────────────────╮
│ 1. Bloom Filter (内存) │→ 若为不存在,则直接拒绝请求(一定不存在)
╰────────────────────╯
↓
╭────────────────────╮
│ 2. Redis 缓存层 │→ 命中则直接返回数据
╰────────────────────╯
↓
╭────────────────────╮
│ 3. MySQL 等数据库层 │→ 真正查询并回填缓存
╰────────────────────╯
✅ 二、这个组合能解决什么问题?
🌩️ 1. 防止 缓存穿透
某些恶意或错误请求频繁查询根本不存在的 key,直接穿透到 DB:
- ❌ Redis 中没有
- ❌ 数据库也没有
- ✅ Bloom Filter 先挡掉大部分不存在的 key!
🎯 2. 降低 Redis 压力
由于 Bloom Filter 是在本地内存中执行 bit 数组判断,速度极快,能挡掉大量无效请求,Redis 能只处理更“可能真实”的 key。
📉 3. 缓解 数据库压力
对于 Bloom Filter 误判漏掉的少量 key,通过 Redis 兜一层,减轻 DB 查询频率。
✅ 三、示例流程(以用户请求 GET /product?id=12345
为例)
-
Bloom Filter 判断 key=product:12345 是否可能存在
- 若明确不存在 → 直接拒绝请求,返回空
- 若可能存在 → 继续查询缓存
-
Redis 查询是否缓存该商品信息
- 若命中 → 返回数据
- 若未命中 → 进入数据库查询
-
MySQL 查询真实数据
- 若查到 → 返回数据 + 写入 Redis 缓存
- 若没查到 → 返回空 + 可以选择写入 Redis 空值(防止短时间内重复穿透)
✅ 四、配套策略建议
项目 | 建议配置 |
---|---|
Bloom Filter | 本地内存(Guava、Caffeine 或 Redis 模块),定期重建,或 Counting Bloom |
Redis 缓存 | 设置过期时间,热点数据可以用永久 key,冷数据可自动淘汰 |
缓存空值 | Redis 可以存 "__null__" 作为空占位,避免重复穿透 |
MySQL 查询后 | 若查到 → 缓存;若查不到 → 空值或异步补数据 |
熔断机制 | Redis 和 DB 出现问题时快速 fail,避免雪崩 |
数据同步 | 可用 Binlog、定时任务、消息队列等同步数据到 Bloom Filter |
✅ 五、代码角度如何组织?(伪代码)
public Product getProduct(String id) {
// 1. bloom filter 判断
if (!bloomFilter.mightContain(id)) {
return null; // 一定不存在,直接返回
}
// 2. redis 查询
String redisKey = "product:" + id;
Product cached = redis.get(redisKey);
if (cached != null) {
return cached.equals("__null__") ? null : cached;
}
// 3. DB 查询
Product dbProduct = mysql.queryProductById(id);
if (dbProduct != null) {
redis.set(redisKey, dbProduct, EXPIRE_TIME);
} else {
redis.set(redisKey, "__null__", SHORT_EXPIRE); // 空值防穿透
}
return dbProduct;
}
✅ 六、实际案例
公司 | 应用 Bloom Filter 的场景 |
---|---|
淘宝 | 商品推荐防止重复展示 |
美团 | 高频业务请求去重、请求过滤 |
百度 | 网页索引的去重与过滤 |
腾讯 | 海量用户账号黑名单快速判断 |
今日头条 | 推荐系统历史记录过滤 |
✅ 七、小结
优势 | 实现方式 |
---|---|
✅ 快速拦截不存在的 key | Bloom Filter |
✅ 缓存热点数据、减少 DB 压力 | Redis |
✅ 保证数据最终一致性 | MySQL 兜底 |
✅ 减少误判副作用 | Redis 空值缓存,或异步纠正 |
✅ 易于扩展 | 可以引入分布式布隆过滤器、Cuckoo Filter 等新方案 |
如果你用在自动化测试平台中,比如:判断某个测试任务是否已执行、某个任务 ID 是否存在于计划中等场景,这种架构也完全适用。
需要我帮你写具体的实现代码(Java + Redis + Guava BloomFilter)也可以告诉我 👍
除了上面的问题之外,bloom-filter 使用还有哪些要注意的点?
Bloom Filter 虽然强大,但它也不是万能的,使用时确实需要注意一些 陷阱、限制和设计决策。下面给你系统整理一下:
✅ 一、核心特性再强调一次(理解这些才能避免误用)
特性 | 说明 |
---|---|
✅ 只支持新增,不支持删除(除非用 Counting Bloom) | 因为不同元素可能共享 bit 位,删除容易误删 |
✅ 存在一定误判(False Positive) | 会误判不存在的 key 为“可能存在” |
✅ 一定不会漏判(False Negative) | 真正存在的 key 一定会返回“可能存在” |
✅ 无法列出所有已添加的元素 | 因为 Bloom Filter 不存储元素,只存 bit 信息 |
🧱 二、使用 Bloom Filter 时要特别注意的点
1️⃣ 哈希函数的选择 至关重要
- 哈希函数要独立、分布均匀
- 通常选择 MurmurHash、Guava 提供的
Funnels
等高质量 hash - 太弱的 hash 函数会导致 bit 位集中,误判率暴涨
💡 业界常见做法:
- 使用一个强哈希函数 + 多次扰动(例如 hash1(x), hash1(x+salt1), hash1(x+salt2)…)
2️⃣ 合理配置参数:m
, k
, n
m
: bit 数组长度k
: 哈希函数个数n
: 预期元素个数
推荐使用误判率公式进行预估(误判率 fp):
\[fp \approx \left(1 - e^{-kn/m}\right)^k\]🧠 最优的哈希函数数量 k
为:
👉 若 m/n 比例不足、k 设置不当,会让误判率严重偏高。
3️⃣ 误判带来的影响不能被忽视
即使误判率只有 0.1%,在高频系统中也很可观:
- 每天 1 亿请求 → 误判 10 万次
- 如果误判带来“请求落库”,会造成数据库雪崩
💡 解决办法:
- 使用 Redis 缓存空值
- 过滤后仍需要“二次兜底验证”
4️⃣ 容量是固定的,一旦满了误判率会迅速升高
Bloom Filter 是容量敏感型结构,当加入的数据远多于预计值 n
时:
- 会导致 bit 数组变得越来越“稠密”
- 误判率接近 100%
🧩 解决办法:
- 用 Scalable Bloom Filter(支持自动扩容)
- 或者当使用量超过某阈值时,主动 rebuild
5️⃣ 不支持取出数据(不是传统意义的集合)
- 无法知道到底存了哪些元素
- 无法遍历、删除、查看 key 的元信息
- 因此不能替代哈希集合(Set)或数据库
6️⃣ 不适合对数据准确性要求极高的场景
⚠️ 禁止 Bloom Filter 使用场景:
- 支付、交易、风控、金融等不容许误判的业务
- 权限控制(误判可能导致非法访问)
- 安全认证(误判就是漏洞)
7️⃣ 分布式部署注意“版本同步”问题
在多个实例上共享 Bloom Filter(如 Redis Bloom 模块)时,要注意:
- 所有服务使用同一个版本的 Bloom Filter
- 否则可能因版本不一致、bit 数据未同步 → 判断结果不同步
💡 建议:
- 使用 Redis Bloom、Guava BloomFilter 并封装成一个独立服务
- 保证集群间读写一致性(用消息队列或定时同步)
8️⃣ 冷启动期间,bloom filter 是“空的”
系统刚启动时,Bloom Filter 可能还没加载完历史数据,导致:
- 本应存在的 key 被误判为“不存在”
- 从而引发“该去查数据库的却被挡掉了”
💡 解决方法:
- 冷启动时延迟启用 Bloom Filter
- 启动阶段查询 Redis 和 DB,再逐步灌入 Bloom Filter
✅ 三、Bloom Filter 的使用 checklist
项 | 是否处理 |
---|---|
是否预估了最大数据量 n ? |
✅ |
是否根据目标误判率计算了 m 和 k ? |
✅ |
哈希函数是否足够分散? | ✅ |
是否考虑误判的后果? | ✅ |
是否设计了重建机制? | ✅ |
是否有冷启动方案? | ✅ |
是否记录了 Bloom Filter 的“构建版本”? | ✅ |
是否使用 Redis 空值/缓存兜底? | ✅ |
✅ 四、推荐工具库和组件
工具 | 描述 |
---|---|
Guava BloomFilter | Java,简单易用,适合本地内存构建 |
Caffeine + BloomFilter | 本地缓存 + 过滤器结合 |
RedisBloom | Redis 模块,支持分布式部署和计数功能 |
Apache Commons BloomFilter | Java 系列工具 |
Scalable BloomFilter(Google 论文) | 自动扩容版 Bloom Filter |
Roaring Bitmap | 另一个压缩位图过滤器,用于整数集过滤 |
如果你有具体使用场景(比如某模块接口防穿透、任务ID去重、日志过滤等),我可以基于实际场景帮你再优化一版配置和设计方案。