文章目录
前言:
布隆过滤可以解决最经典的缓存击穿的问题。可以防止恶意请求不存在的key,导致大量恶意请求打到mysql数据库。
布隆过滤器的核心特性是:
不会漏判(如果元素实际存在,一定会判断为存在)
可能误判(如果元素实际不存在,可能会判断为存在)
一、技术选型:
在Java中,布隆过滤器的第三方实现有多种,常见的包括以下几种:
- Google Guava:Guava库中提供了
BloomFilter
实现,这是最常用的布隆过滤器之一。它支持自定义预期插入数量、误判率等参数,使用简单且性能稳定。 - Apache Commons Commons Collections Collections:该库中也有布隆过滤器的实现,不过相比Guava,其功能和灵活性稍逊一筹。
- Redisson:如果你在使用Redis,Redisson提供了分布式环境下的布隆过滤器实现,适合分布式场景。
- Hutool:国产的Java工具类库Hutool也包含了布隆过滤器的实现,API设计简洁,易于上手。
- Tair:阿里开源的分布式存储框架Tair中也有布隆过滤器的实现,适用于特定的分布式存储场景。
结论:
单机 模式 :
推荐使用谷歌的 guava 。
分布式模式:
推荐使用 redisson,它能在集群环境下保持数据一致性。
二、实战案例
先预热10万个key,用100个key做测试并输出准确率
引入依赖
<!-- Redisson 核心依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.5</version>
</dependency>
redis配置类 RedissonConfig
package test.bloomfilter;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Redisson配置类
* 配置Redisson客户端连接Redis服务器
*
* @author 架构师
* @date 2025-09-11
*/
@Configuration
public class RedissonConfig {
/**
* 创建Redisson客户端实例
* 此处使用单节点Redis配置,实际生产环境可根据需求修改为集群或哨兵模式
* @return RedissonClient实例
*/
@Bean
public RedissonClient redissonClient() {
// 创建配置对象
Config config = new Config();
// 配置单节点Redis,实际使用时替换为真实的Redis地址
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0)
.setConnectionPoolSize(16)
.setConnectionMinimumIdleSize(8);
// 根据配置创建Redisson客户端实例
return Redisson.create(config);
}
}
业务接口类 RedissonBloomFilterService
package test.bloomfilter;
import java.util.Collection;
/**
* 布隆过滤器服务接口
* 提供添加元素、批量添加和判断元素是否存在的功能
*
* @author 架构师
* @date 2025-09-11
*/
public interface RedissonBloomFilterService {
/**
* 初始化布隆过滤器
*
* @param expectedInsertions 预期插入的元素数量
* @param falseProbability 可接受的误判概率
*/
void init(long expectedInsertions, double falseProbability);
/**
* 向布隆过滤器中添加单个元素
*
* @param element 要添加的元素
* @return 是否添加成功
*/
boolean add(String element);
/**
* 向布隆过滤器中批量添加元素
*
* @param elements 要添加的元素集合
* @return 是否全部添加成功
*/
boolean addAll(Collection<String> elements);
/**
* 判断元素是否可能存在于布隆过滤器中
*
* @param element 要判断的元素
* @return true: 可能存在, false: 一定不存在
*/
boolean contains(String element);
}
接口实现
package test.bloomfilter;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.Collection;
/**
* Redisson布隆过滤器服务实现类
* 基于Redisson客户端实现布隆过滤器功能
*
* @author 架构师
* @date 2025-09-11
*/
@Service
@RequiredArgsConstructor
public class RedissonBloomFilterServiceImpl implements RedissonBloomFilterService {
private final RedissonClient redissonClient;
private RBloomFilter<String> bloomFilter;
private static final String BLOOM_FILTER_NAME = "custom_bloom_filter";
/**
* 初始化布隆过滤器
*
* @param expectedInsertions 预期插入的元素数量
* @param falseProbability 可接受的误判概率
*/
@Override
public void init(long expectedInsertions, double falseProbability) {
// 获取或创建布隆过滤器
bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_NAME);
// 初始化布隆过滤器参数
// 注意:如果布隆过滤器已存在,多次调用tryInit不会改变其配置
bloomFilter.tryInit(expectedInsertions, falseProbability);
}
/**
* 向布隆过滤器中添加单个元素
*
* @param element 要添加的元素
* @return 是否添加成功
*/
@Override
public boolean add(String element) {
validateBloomFilterInitialized();
return bloomFilter.add(element);
}
/**
* 向布隆过滤器中批量添加元素
*
* @param elements 要添加的元素集合
* @return 是否全部添加成功
*/
@Override
public boolean addAll(Collection<String> elements) {
validateBloomFilterInitialized();
boolean allAdded = true;
for (String element : elements) {
if (!bloomFilter.add(element)) {
allAdded = false;
}
}
return allAdded;
}
/**
* 判断元素是否可能存在于布隆过滤器中
*
* @param element 要判断的元素
* @return true: 可能存在, false: 一定不存在
*/
@Override
public boolean contains(String element) {
validateBloomFilterInitialized();
return bloomFilter.contains(element);
}
/**
* 验证布隆过滤器是否已初始化
* 若未初始化则抛出IllegalStateException异常
*/
private void validateBloomFilterInitialized() {
if (bloomFilter == null) {
throw new IllegalStateException("布隆过滤器尚未初始化,请先调用init方法");
}
}
}
测试类 BloomFilterTestMain
package test.bloomfilter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
* 布隆过滤器测试主类
* 用于测试布隆过滤器的准确性和性能
* @author 架构师
* @date 2025-09-11
*/
public class BloomFilterTestMain {
/**
* 预热数据量
*/
private static final int WARMUP_DATA_SIZE = 100000;
/**
* 测试数据量
*/
private static final int TEST_DATA_SIZE = 100;
/**
* 测试数据中存在的数据比例 (0.5表示50%的数据存在于预热数据中)
*/
private static final double EXIST_RATIO = 0.5;
/**
* 主方法,执行布隆过滤器测试
*
* @param args 命令行参数
*/
public static void main(String[] args) {
// 初始化Spring上下文
ApplicationContext context = new AnnotationConfigApplicationContext(RedissonConfig.class, RedissonBloomFilterServiceImpl.class);
// 获取布隆过滤器服务实例
RedissonBloomFilterService bloomFilterService = context.getBean(RedissonBloomFilterServiceImpl.class);
// 初始化布隆过滤器:预期10万个元素,误判率0.01
bloomFilterService.init(WARMUP_DATA_SIZE, 0.01);
// 生成预热数据
Set<String> warmupData = generateRandomKeys(WARMUP_DATA_SIZE);
// 预热布隆过滤器
long startTime = System.currentTimeMillis();
boolean allAdded = bloomFilterService.addAll(warmupData);
long warmupTime = System.currentTimeMillis() - startTime;
System.out.printf("预热完成,添加了 %d 个元素,耗时 %d ms,全部添加成功: %b%n",
WARMUP_DATA_SIZE, warmupTime, allAdded);
// 生成测试数据
TestData testData = generateTestData(warmupData);
// 执行测试
TestResult result = executeTest(bloomFilterService, testData);
// 计算并输出准确率
printTestResult(result);
}
/**
* 生成随机的字符串作为测试key
*
* @param count 生成的key数量
* @return 包含指定数量随机key的集合
*/
private static Set<String> generateRandomKeys(int count) {
Set<String> keys = new HashSet<>(count);
Random random = new Random();
while (keys.size() < count) {
// 生成随机的10位数字作为key
int number = random.nextInt(1000000000); // 生成0到999999999之间的随机数
String key = String.format("key_%010d", number);
keys.add(key);
}
return keys;
}
/**
* 生成测试数据,包含一部分存在于预热数据中的key和一部分不存在的key
*
* @param warmupData 预热数据集合
* @return 测试数据对象,包含测试key列表和每个key是否真实存在的标记
*/
private static TestData generateTestData(Set<String> warmupData) {
List<String> testKeys = new ArrayList<>(TEST_DATA_SIZE);
List<Boolean> actualExists = new ArrayList<>(TEST_DATA_SIZE);
Random random = new Random();
// 从预热数据中选取一部分作为存在的测试数据
int existCount = (int) (TEST_DATA_SIZE * EXIST_RATIO);
List<String> existingKeys = new ArrayList<>(warmupData).subList(0, existCount);
// 添加存在的测试数据
for (String key : existingKeys) {
testKeys.add(key);
actualExists.add(true);
}
// 生成不存在的测试数据
Set<String> nonExistingKeys = new HashSet<>();
while (nonExistingKeys.size() < TEST_DATA_SIZE - existCount) {
String key = generateRandomKey(random);
if (!warmupData.contains(key)) {
nonExistingKeys.add(key);
}
}
// 添加不存在的测试数据
for (String key : nonExistingKeys) {
testKeys.add(key);
actualExists.add(false);
}
return new TestData(testKeys, actualExists);
}
/**
* 生成单个随机key
*
* @param random 随机数生成器
* @return 随机生成的key
*/
private static String generateRandomKey(Random random) {
int number = random.nextInt(1000000000); // 生成0到999999999之间的随机数
return String.format("key_%010d", number);
}
/**
* 执行布隆过滤器测试
* @param bloomFilterService 布隆过滤器服务
* @param testData 测试数据
* @return 测试结果,包含各种统计信息
*/
private static TestResult executeTest(RedissonBloomFilterService bloomFilterService, TestData testData) {
int truePositive = 0; // 实际存在,判断存在
int trueNegative = 0; // 实际不存在,判断不存在
int falsePositive = 0; // 实际不存在,判断存在(误判)
int falseNegative = 0; // 实际存在,判断不存在(漏判)
long startTime = System.currentTimeMillis();
for (int i = 0; i < testData.testKeys.size(); i++) {
String key = testData.testKeys.get(i);
boolean actual = testData.actualExists.get(i);
boolean predicted = bloomFilterService.contains(key);
if (actual) {
if (predicted) {
truePositive++;
} else {
falseNegative++;
}
} else {
if (predicted) {
falsePositive++;
} else {
trueNegative++;
}
}
}
long testTime = System.currentTimeMillis() - startTime;
return new TestResult(
truePositive, trueNegative,
falsePositive, falseNegative,
testTime
);
}
/**
* 打印测试结果
*
* @param result 测试结果对象
*/
private static void printTestResult(TestResult result) {
System.out.println("\n测试结果统计:");
System.out.printf("测试数据总数: %d%n", TEST_DATA_SIZE);
System.out.printf("实际存在的key数量: %d%n", result.truePositive + result.falseNegative);
System.out.printf("实际不存在的key数量: %d%n", result.trueNegative + result.falsePositive);
System.out.println("-----");
System.out.printf("真阳性 (实际存在,判断存在): %d%n", result.truePositive);
System.out.printf("真阴性 (实际不存在,判断不存在): %d%n", result.trueNegative);
System.out.printf("假阳性 (实际不存在,判断存在 - 误判): %d%n", result.falsePositive);
System.out.printf("假阴性 (实际存在,判断不存在 - 漏判): %d%n", result.falseNegative);
System.out.println("-----");
// 计算准确率:(真阳性 + 真阴性) / 总测试数
double accuracy = (double) (result.truePositive + result.trueNegative) / TEST_DATA_SIZE;
// 计算精确率:真阳性 / (真阳性 + 假阳性)
double precision = result.truePositive + result.falsePositive > 0
? (double) result.truePositive / (result.truePositive + result.falsePositive)
: 1.0;
// 计算召回率:真阳性 / (真阳性 + 假阴性)
double recall = result.truePositive + result.falseNegative > 0
? (double) result.truePositive / (result.truePositive + result.falseNegative)
: 1.0;
System.out.printf("准确率: %.2f%%%n", accuracy * 100);
System.out.printf("精确率: %.2f%%%n", precision * 100);
System.out.printf("召回率: %.2f%%%n", recall * 100);
System.out.printf("测试耗时: %d ms%n", result.testTime);
}
/**
* 测试数据封装类
*/
private static class TestData {
List<String> testKeys;
List<Boolean> actualExists;
TestData(List<String> testKeys, List<Boolean> actualExists) {
this.testKeys = testKeys;
this.actualExists = actualExists;
}
}
/**
* 测试结果封装类
*/
private static class TestResult {
int truePositive; // 真阳性
int trueNegative; // 真阴性
int falsePositive; // 假阳性
int falseNegative; // 假阴性
long testTime; // 测试耗时(ms)
TestResult(int truePositive, int trueNegative,
int falsePositive, int falseNegative,
long testTime) {
this.truePositive = truePositive;
this.trueNegative = trueNegative;
this.falsePositive = falsePositive;
this.falseNegative = falseNegative;
this.testTime = testTime;
}
}
}
测试结果
-----
预热完成,添加了 100000 个元素,耗时 7383 ms,全部添加成功: false
测试结果统计:
测试数据总数: 100
实际存在的key数量: 50
实际不存在的key数量: 50
-----
真阳性 (实际存在,判断存在): 50
真阴性 (实际不存在,判断不存在): 39
假阳性 (实际不存在,判断存在 - 误判): 11
假阴性 (实际存在,判断不存在 - 漏判): 0
-----
准确率: 89.00%
精确率: 81.97%
召回率: 100.00%
测试耗时: 10 ms
-----
如果文章对你有一点点帮助,欢迎【点赞、留言、+ 关注】
您的关注是我创作的动力!若有疑问/交流/需求,欢迎留言/私聊!
多一个朋友多一条路!