SpringBoot+Redis+布隆过滤器防止缓存穿透
作者:白仑色
本文介绍了一个基于Spring Boot、Redis和Guava布隆过滤器的高并发系统缓存穿透解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
✅ 项目概述
在高并发系统中,缓存穿透 是一个经典问题:当恶意请求或业务逻辑查询一个数据库中不存在的 Key,由于缓存中也没有,请求会直接打到数据库,导致数据库压力激增,甚至宕机。
本项目使用 Spring Boot + Redis + Guava 布隆过滤器 实现一个完整的解决方案,有效防止缓存穿透,提升系统稳定性与性能。
📌 项目结构
bloom-filter-demo/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/bloomfilterdemo/ │ │ │ ├── controller/ │ │ │ │ └── ProductController.java │ │ │ ├── service/ │ │ │ │ └── ProductService.java │ │ │ ├── config/ │ │ │ │ └── RedisBloomFilterConfig.java │ │ │ └── BloomFilterDemoApplication.java │ │ └── resources/ │ │ ├── application.yml │ │ └── data/products.csv # 模拟商品数据 ├── pom.xml └── README.md
📌 第一步:添加 Maven 依赖
<!-- pom.xml --> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Boot Data Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Guava(提供 BloomFilter) --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> </dependency> <!-- Lombok(简化代码) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!-- CSV 解析(用于初始化数据) --> <dependency> <groupId>com.opencsv</groupId> <artifactId>opencsv</artifactId> <version>5.7.1</version> </dependency> </dependencies>
📌 第二步:配置文件(application.yml)
server: port: 8080 spring: redis: host: localhost port: 6379 password: lettuce: pool: max-active: 8 max-idle: 8 timeout: 5s # Redis 序列化配置(可选) cache: type: redis
📌 第三步:创建商品实体类
// src/main/java/com/example/bloomfilterdemo/entity/Product.java package com.example.bloomfilterdemo.entity; import lombok.Data; @Data public class Product { private String id; private String name; private Double price; private String category; }
📌 第四步:配置布隆过滤器与 Redis
// src/main/java/com/example/bloomfilterdemo/config/RedisBloomFilterConfig.java package com.example.bloomfilterdemo.config; import com.google.common.hash.Funnels; import com.google.common.util.concurrent.Uninterruptibles; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import com.google.common.hash.BloomFilter; import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; @Configuration public class RedisBloomFilterConfig { @Autowired private StringRedisTemplate stringRedisTemplate; // 布隆过滤器(存储所有存在的商品ID) private BloomFilter<String> bloomFilter; // Redis Key private static final String BLOOM_FILTER_KEY = "bloom:products"; @Bean public BloomFilter<String> bloomFilter() { // 预估元素数量:10万,误判率:0.01% this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 100000, 0.0001); return bloomFilter; } /** * 项目启动时初始化布隆过滤器 * 实际项目中可从数据库或缓存中加载所有存在的 ID */ @PostConstruct public void initBloomFilter() { // 模拟:从数据库加载所有商品ID for (int i = 1; i <= 10000; i++) { String productId = "P" + i; bloomFilter.put(productId); // 同时将真实数据存入 Redis(模拟缓存) stringRedisTemplate.opsForValue().set("product:" + productId, "Product Data " + productId); } System.out.println("✅ 布隆过滤器初始化完成,加载 10000 个商品ID"); } /** * 手动添加新商品到布隆过滤器(可选) */ public void addProductToBloom(String productId) { bloomFilter.put(productId); // 异步更新 Redis(或持久化到 DB) stringRedisTemplate.opsForValue().set("product:" + productId, "New Product Data"); } }
📌 第五步:商品服务层
// src/main/java/com/example/bloomfilterdemo/service/ProductService.java package com.example.bloomfilterdemo.service; import com.example.bloomfilterdemo.config.RedisBloomFilterConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @Service public class ProductService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedisBloomFilterConfig bloomFilterConfig; /** * 查询商品信息(带布隆过滤器防护) * @param productId * @return 商品信息或 null */ public String getProduct(String productId) { // 1. 先通过布隆过滤器判断是否存在 if (!bloomFilterConfig.bloomFilter.mightContain(productId)) { System.out.println("❌ 布隆过滤器判定:商品ID " + productId + " 不存在(可能误判)"); return null; // 直接返回,避免查缓存和数据库 } // 2. 布隆过滤器认为可能存在,查 Redis 缓存 String cacheKey = "product:" + productId; String productData = stringRedisTemplate.opsForValue().get(cacheKey); if (productData != null) { System.out.println("✅ Redis 缓存命中:" + productId); return productData; } // 3. 缓存未命中,查数据库(此处模拟) String dbData = queryFromDatabase(productId); if (dbData != null) { // 4. 写入缓存(设置过期时间) stringRedisTemplate.opsForValue().set(cacheKey, dbData, 30, java.util.concurrent.TimeUnit.MINUTES); System.out.println("📦 数据库查询并写入缓存:" + productId); return dbData; } else { // 5. 数据库也不存在,可选择缓存空值(防缓存穿透二次攻击) // stringRedisTemplate.opsForValue().set(cacheKey, "", 1, TimeUnit.MINUTES); System.out.println("❌ 数据库查询失败:商品ID " + productId + " 不存在"); return null; } } /** * 模拟数据库查询 */ private String queryFromDatabase(String productId) { // 模拟:只有 P1 ~ P10000 存在 try { Thread.sleep(10); // 模拟数据库延迟 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (productId.matches("P\\d{1,5}") && Integer.parseInt(productId.substring(1)) <= 10000) { return "【数据库】商品详情 - " + productId; } return null; } }
📌 第六步:控制器层
// src/main/java/com/example/bloomfilterdemo/controller/ProductController.java package com.example.bloomfilterdemo.controller; import com.example.bloomfilterdemo.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class ProductController { @Autowired private ProductService productService; /** * 查询商品信息 * 测试正常请求:http://localhost:8080/product/P123 * 测试穿透请求:http://localhost:8080/product/P999999 */ @GetMapping("/product/{id}") public String getProduct(@PathVariable String id) { long start = System.currentTimeMillis(); String result = productService.getProduct(id); long cost = System.currentTimeMillis() - start; if (result == null) { return "商品不存在,耗时:" + cost + "ms"; } return result + "(耗时:" + cost + "ms)"; } }
📌 第七步:启动类
// src/main/java/com/example/bloomfilterdemo/BloomFilterDemoApplication.java package com.example.bloomfilterdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BloomFilterDemoApplication { public static void main(String[] args) { SpringApplication.run(BloomFilterDemoApplication.class, args); System.out.println("🚀 Spring Boot + Redis + 布隆过滤器 项目启动成功!"); System.out.println("🎯 访问测试:http://localhost:8080/product/P123"); System.out.println("🎯 穿透测试:http://localhost:8080/product/P999999"); } }
📌 第八步:测试与验证
1. 启动项目
确保 Redis 服务已运行,然后启动 Spring Boot 项目。
2. 正常请求(缓存命中)
http://localhost:8080/product/P123
输出:
【数据库】商品详情 - P123(耗时:15ms)
# 第二次请求
商品详情 - P123(耗时:2ms) # Redis 缓存命中
3. 缓存穿透请求(布隆过滤器拦截)
http://localhost:8080/product/P999999
输出:
商品不存在,耗时:1ms
✅ 关键点:该请求未进入缓存查询,也未访问数据库,直接被布隆过滤器拦截,耗时极低。
✅ 方案优势总结
优势 | 说明 |
---|---|
⚡ 高效拦截 | 不存在的 Key 被布隆过滤器快速拦截,避免查缓存和数据库 |
💾 内存友好 | 布隆过滤器空间效率高,10万数据仅需几十 KB |
🛡️ 高并发防护 | 有效防止恶意刷不存在的 Key 导致数据库雪崩 |
🔄 可扩展 | 支持动态添加新数据(如新增商品) |
📚 注意事项与优化建议
- 误判率权衡:布隆过滤器有误判率(False Positive),但不会漏判。可根据业务调整大小和误判率。
- 数据一致性:当数据库新增数据时,需同步更新布隆过滤器。
- 替代方案:也可使用 Redis 自带的 RedisBloom 模块(需编译安装),支持
BF.ADD
、BF.EXISTS
等命令。 - 缓存空值:对于高频但不存在的 Key,可结合“缓存空值 + 短 TTL”进一步优化。
📚 推荐阅读
到此这篇关于SpringBoot+Redis+布隆过滤器防止缓存穿透的文章就介绍到这了,更多相关SpringBoot Redis 布隆过滤器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!