? 項目概述
在高并發系統中,緩存穿透 是一個經典問題:當惡意請求或業務邏輯查詢一個數據庫中不存在的 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: 8080spring:redis:host: localhostport: 6379password: lettuce:pool:max-active: 8max-idle: 8timeout: 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 {@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 布隆過濾器(存儲所有存在的商品ID)private BloomFilter<String> bloomFilter;// Redis Keyprivate static final String BLOOM_FILTER_KEY = "bloom:products";@Beanpublic BloomFilter<String> bloomFilter() {// 預估元素數量:10萬,誤判率:0.01%this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 100000, 0.0001);return bloomFilter;}/*** 項目啟動時初始化布隆過濾器* 實際項目中可從數據庫或緩存中加載所有存在的 ID*/@PostConstructpublic void initBloomFilter() {// 模擬:從數據庫加載所有商品IDfor (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 {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate 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 {@Autowiredprivate 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”進一步優化。
📚 推薦閱讀
- Guava BloomFilter 官方文檔