1. 布隆过滤器简介
- 作用:高效判断某个元素是否可能存在于集合中(存在“可能误判”,但“不存在”是确定的)。
- 适用场景:缓存穿透防护、爬虫 URL 去重、防止重复请求等。
- 特点:
- 空间效率高,但有一定误判率。
- 不支持删除操作(需结合 Counting Bloom Filter)。
2、布隆过滤器核心原理
2.1 数据结构基础
- 位数组(Bit Array)
布隆过滤器的核心是一个长度为m
的二进制位数组(所有位初始为0)。 - 哈希函数集合
使用k
个不同的哈希函数,每个函数将元素映射到位数组的某个位置(范围:0 ~ m-1)。
2.2 操作原理
(1) 添加元素
- 将元素依次通过
k
个哈希函数,得到k
个哈希值。 - 将位数组中这
k
个位置的值设为1。
(2) 判断元素存在性
- 将元素通过同样的
k
个哈希函数,得到k
个位置。 - 若所有位置的值均为1 → 可能存在(存在误判)。
- 若任一位置的值为0 → 一定不存在。
3. Redisson 布隆过滤器实现
3.1 实现机制
-
Redis 存储结构: Redisson 将位数组存储在 Redis 的字符串结构中,通过 SETBIT 和 GETBIT 命令操作位。
-
哈希函数生成: 使用双重哈希(Murmur3 + 线性探测)模拟多个哈希函数的效果,减少实际哈希计算次数。
3.2 添加 Redisson 依赖
<!-- Maven 依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.7</version> <!-- 使用最新稳定版本 -->
</dependency>
3.3 配置 Redisson 客户端
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonConfig {
public static RedissonClient getRedissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("your_password")
.setDatabase(0);
return Redisson.create(config);
}
}
3.4 初始化布隆过滤器
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
public class BloomFilterDemo {
public static void main(String[] args) {
RedissonClient redisson = RedissonConfig.getRedissonClient();
// 初始化布隆过滤器(需指定名称、预期插入数量、误判率)
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user_id_filter");
bloomFilter.tryInit(1000000L, 0.03); // 预期插入 100 万数据,误判率 3%
}
}
3.5 添加元素与检查存在性
// 添加元素
bloomFilter.add("user_12345");
// 检查元素是否存在
boolean exists = bloomFilter.contains("user_67890");
System.out.println("是否存在:" + exists); // 输出 true 或 false
3.6 参数调优建议
参数 | 说明 | 推荐值 |
---|---|---|
expectedInsertions | 预期插入的元素数量,影响内存占用和哈希函数数量。 | 根据业务规模预估 + 20% 冗余 |
falseProbability | 可接受的误判率(范围:0 < 误判率 < 1),值越小,内存占用越高。 | 0.01–0.05(1%~5%) |
3.7 注意事项
(1) 误判率与容量平衡
- 内存与误判率关系: 误判率越低,所需内存空间越大(需权衡业务容忍度与硬件成本)。
- 容量超限风险: 实际插入元素数量超过预设容量时,误判率会非线性上升,需预留 20%~30% 的冗余容量。
(2) 数据预热
- 初始化加载: 系统启动时需预先将所有合法存在的 key 加载到布隆过滤器中(例如从数据库全量查询并批量插入)。
List<String> allKeys = database.getAllValidKeys();
allKeys.forEach(bloomFilter::add);
(3) 不支持删除
- 标准限制: 传统布隆过滤器无法直接删除元素(删除会导致其他元素存在性误判)
- 替代方案:
Counting Bloom Filter
:通过计数器支持删除,但内存占用更高。Redisson Pro 扩展
:使用 RClusteredBloomFilter 实现类似功能。
(4) 集群模式
- Redis 集群限制: 需确保布隆过滤器的所有操作键落在同一槽位,避免跨节点操作。
- 哈希标签强制路由: 使用 {} 包裹固定标签,保证键分配到同一槽位。
// 示例:所有操作使用 "{filter}" 作为哈希标签
RBloomFilter<String> filter = redisson.getBloomFilter("{filter}user_filter");
3.8 完整示例代码
public class CacheService {
private RBloomFilter<String> bloomFilter;
private RedissonClient redisson;
private Cache<String, Object> localCache;
public CacheService() {
redisson = RedissonConfig.getRedissonClient();
bloomFilter = redisson.getBloomFilter("product_id_filter");
bloomFilter.tryInit(1000000L, 0.01);
}
public Object getProduct(String productId) {
// 1. 先检查布隆过滤器
if (!bloomFilter.contains(productId)) {
return null; // 直接返回,避免查询数据库
}
// 2. 查询本地缓存
Object value = localCache.get(productId);
if (value != null) {
return value;
}
// 3. 查询数据库
value = database.query(productId);
if (value != null) {
localCache.put(productId, value);
} else {
// 防止缓存穿透:将不存在的 key 也加入过滤器
bloomFilter.add(productId);
}
return value;
}
}
3.9 应用场景建议
场景 | 配置建议 | 示例 |
---|---|---|
缓存穿透防护 | 预期插入量 = 数据库总数据量 × 1.2 | 商品ID过滤(百万级) |
爬虫 URL 去重 | 预期插入量 = 预估抓取量 × 2 | 网页URL去重(千万级) |
风控黑名单过滤 | 低误判率(0.1%~1%) + 定期重建 | 欺诈用户ID过滤(高频更新) |
4. 高级特性与注意事项
4.1 动态扩容(Redisson Pro)
- 特性:当实际插入量超过预期时自动扩展位数组。
- 启用方式:
bloomFilter.setMaxSize(5000000L); // 最大允许插入500万数据
4.2 集群模式支持
// 使用哈希标签确保所有操作路由到同一槽位
RBloomFilter<String> clusterFilter = redisson.getBloomFilter("{filter}user_filter");
4.3 性能指标
操作 | 时间复杂度 | 网络请求次数 |
---|---|---|
add() | O(k) | k |
contains() | O(k) | k |
4.4 使用限制
-
不支持删除操作:传统布隆过滤器无法安全删除元素(需使用 Counting Bloom Filter)。
-
数据预热要求:必须预先加载所有合法 key,否则会误判真实存在的 key 为不存在。
-
容量规划:实际插入量超过预期时,误判率会非线性上升。
拓展:Counting Bloom Filter 实现布隆过滤器的删除功能
1、传统布隆过滤器的删除问题
- 位数组不可逆:传统布隆过滤器使用二进制位数组标记元素存在性。
- 删除风险:直接删除元素(置零对应位)会导致共享该位的其他元素被误判为不存在。
示例:
- 元素 A 哈希到位置 [1,3,5],元素 B 哈希到 [3,5,7]
- 删除 A 时置零位 [1,3,5] → B 的检查结果变为“不存在”(因位3和5被清零)
2. Counting Bloom Filter (CBF) 原理
2.1 数据结构改进
- 计数器数组:将传统位数组中的二进制位扩展为 整数计数器(通常4-8位)。
- 操作规则:
操作 行为 说明 添加元素 对应计数器 +1 标记元素的“存在性” 删除元素 对应计数器 -1 安全解除元素标记 查询元素 所有计数器 >0 可能存在(仍有误判)
2.2 工作流程
初始化计数器数组:[0,0,0,0,0,0,0,0,0,0]
添加元素A(哈希到1,3,5)→ [0,1,0,1,0,1,0,0,0,0]
添加元素B(哈希到3,5,7)→ [0,1,0,2,0,2,0,1,0,0]
删除元素A → [0,0,0,1,0,1,0,1,0,0]
查询元素B → 检查位3(1)、5(1)、7(1) → 存在(正确)
3. 关键问题与解决方案
3.1 计数器溢出
-
风险:计数器达到最大值后溢出(如4位计数器最大值为15)。
-
解决方案:
-
增加位数:使用8位或16位计数器(取值范围0-255或0~65535)。
-
饱和计数:达到最大值后不再递增(牺牲准确性,避免溢出)。
-
3.2 性能优化
优化方向 | 实现方法 | 效果 |
---|---|---|
并行哈希计算 | 使用 SIMD 指令或 GPU 加速哈希计算 | 提升吞吐量 |
内存压缩 | 使用变长编码(如Elias-Fano)存储计数器 | 减少内存占用 |
4. CBF 的优缺点
-
优点:
-
支持删除:可安全删除元素,适用于动态数据集。
-
兼容性高:在传统布隆过滤器基础上扩展,实现简单。
-
-
缺点:
-
内存占用高:计数器数组需要更多存储空间(约传统BF的4-8倍)。
-
误判率略高:计数器溢出或删除操作可能增加误判概率。
-
5. 应用场景
场景 | 说明 | 示例 |
---|---|---|
动态黑名单系统 | 支持从黑名单中移除用户或IP | 风控系统实时更新黑名单 |
缓存淘汰机制 | 允许删除过期缓存标记 | LRU缓存策略 |
实时流处理 | 统计元素出现频率并支持撤回操作 | 广告点击去重 |