SpringBoot+Redis+布隆過濾器防止緩存穿透
? 項(xiàng)目概述
在高并發(fā)系統(tǒng)中,緩存穿透 是一個經(jīng)典問題:當(dāng)惡意請求或業(yè)務(wù)邏輯查詢一個數(shù)據(jù)庫中不存在的 Key,由于緩存中也沒有,請求會直接打到數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫壓力激增,甚至宕機(jī)。
本項(xiàng)目使用 Spring Boot + Redis + Guava 布隆過濾器 實(shí)現(xiàn)一個完整的解決方案,有效防止緩存穿透,提升系統(tǒng)穩(wěn)定性與性能。
?? 項(xiàng)目結(jié)構(gòu)
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 # 模擬商品數(shù)據(jù) ├── 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 解析(用于初始化數(shù)據(jù)) -->
<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
?? 第三步:創(chuàng)建商品實(shí)體類
// 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() {
// 預(yù)估元素?cái)?shù)量:10萬,誤判率:0.01%
this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 100000, 0.0001);
return bloomFilter;
}
/**
* 項(xiàng)目啟動時初始化布隆過濾器
* 實(shí)際項(xiàng)目中可從數(shù)據(jù)庫或緩存中加載所有存在的 ID
*/
@PostConstruct
public void initBloomFilter() {
// 模擬:從數(shù)據(jù)庫加載所有商品ID
for (int i = 1; i <= 10000; i++) {
String productId = "P" + i;
bloomFilter.put(productId);
// 同時將真實(shí)數(shù)據(jù)存入 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");
}
}
?? 第五步:商品服務(wù)層
// 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;
/**
* 查詢商品信息(帶布隆過濾器防護(hù))
* @param productId
* @return 商品信息或 null
*/
public String getProduct(String productId) {
// 1. 先通過布隆過濾器判斷是否存在
if (!bloomFilterConfig.bloomFilter.mightContain(productId)) {
System.out.println("? 布隆過濾器判定:商品ID " + productId + " 不存在(可能誤判)");
return null; // 直接返回,避免查緩存和數(shù)據(jù)庫
}
// 2. 布隆過濾器認(rèn)為可能存在,查 Redis 緩存
String cacheKey = "product:" + productId;
String productData = stringRedisTemplate.opsForValue().get(cacheKey);
if (productData != null) {
System.out.println("? Redis 緩存命中:" + productId);
return productData;
}
// 3. 緩存未命中,查數(shù)據(jù)庫(此處模擬)
String dbData = queryFromDatabase(productId);
if (dbData != null) {
// 4. 寫入緩存(設(shè)置過期時間)
stringRedisTemplate.opsForValue().set(cacheKey, dbData, 30, java.util.concurrent.TimeUnit.MINUTES);
System.out.println("?? 數(shù)據(jù)庫查詢并寫入緩存:" + productId);
return dbData;
} else {
// 5. 數(shù)據(jù)庫也不存在,可選擇緩存空值(防緩存穿透二次攻擊)
// stringRedisTemplate.opsForValue().set(cacheKey, "", 1, TimeUnit.MINUTES);
System.out.println("? 數(shù)據(jù)庫查詢失?。荷唐稩D " + productId + " 不存在");
return null;
}
}
/**
* 模擬數(shù)據(jù)庫查詢
*/
private String queryFromDatabase(String productId) {
// 模擬:只有 P1 ~ P10000 存在
try {
Thread.sleep(10); // 模擬數(shù)據(jù)庫延遲
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (productId.matches("P\\d{1,5}") && Integer.parseInt(productId.substring(1)) <= 10000) {
return "【數(shù)據(jù)庫】商品詳情 - " + 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 + 布隆過濾器 項(xiàng)目啟動成功!");
System.out.println("?? 訪問測試:http://localhost:8080/product/P123");
System.out.println("?? 穿透測試:http://localhost:8080/product/P999999");
}
}
?? 第八步:測試與驗(yàn)證
1. 啟動項(xiàng)目
確保 Redis 服務(wù)已運(yùn)行,然后啟動 Spring Boot 項(xiàng)目。
2. 正常請求(緩存命中)
http://localhost:8080/product/P123
輸出:
【數(shù)據(jù)庫】商品詳情 - P123(耗時:15ms)
# 第二次請求
商品詳情 - P123(耗時:2ms) # Redis 緩存命中
3. 緩存穿透請求(布隆過濾器攔截)
http://localhost:8080/product/P999999
輸出:
商品不存在,耗時:1ms
? 關(guān)鍵點(diǎn):該請求未進(jìn)入緩存查詢,也未訪問數(shù)據(jù)庫,直接被布隆過濾器攔截,耗時極低。
? 方案優(yōu)勢總結(jié)
| 優(yōu)勢 | 說明 |
|---|---|
| ? 高效攔截 | 不存在的 Key 被布隆過濾器快速攔截,避免查緩存和數(shù)據(jù)庫 |
| ?? 內(nèi)存友好 | 布隆過濾器空間效率高,10萬數(shù)據(jù)僅需幾十 KB |
| ??? 高并發(fā)防護(hù) | 有效防止惡意刷不存在的 Key 導(dǎo)致數(shù)據(jù)庫雪崩 |
| ?? 可擴(kuò)展 | 支持動態(tài)添加新數(shù)據(jù)(如新增商品) |
?? 注意事項(xiàng)與優(yōu)化建議
- 誤判率權(quán)衡:布隆過濾器有誤判率(False Positive),但不會漏判??筛鶕?jù)業(yè)務(wù)調(diào)整大小和誤判率。
- 數(shù)據(jù)一致性:當(dāng)數(shù)據(jù)庫新增數(shù)據(jù)時,需同步更新布隆過濾器。
- 替代方案:也可使用 Redis 自帶的 RedisBloom 模塊(需編譯安裝),支持
BF.ADD、BF.EXISTS等命令。 - 緩存空值:對于高頻但不存在的 Key,可結(jié)合“緩存空值 + 短 TTL”進(jìn)一步優(yōu)化。
?? 推薦閱讀
到此這篇關(guān)于SpringBoot+Redis+布隆過濾器防止緩存穿透的文章就介紹到這了,更多相關(guān)SpringBoot Redis 布隆過濾器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何查找YUM安裝的JAVA_HOME環(huán)境變量詳解
這篇文章主要給大家介紹了關(guān)于如何查找YUM安裝的JAVA_HOME環(huán)境變量的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10
jdbc鏈接遠(yuǎn)程數(shù)據(jù)庫進(jìn)行修改url操作
這篇文章主要為大家詳細(xì)介紹了jdbc鏈接遠(yuǎn)程數(shù)據(jù)庫進(jìn)行修改url操作,感興趣的小伙伴們可以參考一下2016-06-06
Java利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏
這篇文章主要介紹了利用Jackson序列化實(shí)現(xiàn)數(shù)據(jù)脫敏,首先在需要進(jìn)行脫敏的VO字段上面標(biāo)注相關(guān)脫敏注解,具體實(shí)例代碼文中給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-10-10
詳解Spring的autowire-candidate設(shè)計(jì)
現(xiàn)在的Spring應(yīng)用通常都是基于注解開發(fā),但是對Spring感興趣的同學(xué)可以借助Spring早期基于Xml配置的各種運(yùn)用來加深對Spring框架內(nèi)部的理解和體會Spring框架的設(shè)計(jì)之妙。這篇文章我們就來談?wù)刋ml配置之default-autowire-candidates2021-06-06
修改SpringBoot 中MyBatis的mapper.xml文件位置的過程詳解
由于MyBatis默認(rèn)的mapper.xml的掃描位置是resource文件下,但是不可能整個項(xiàng)目的mapper.xml文件都放在resource下,如果文件較少還行,但是如果文件比較多,太麻煩了,所以本文給大家介紹了修改SpringBoot 中MyBatis的mapper.xml文件位置的過程,需要的朋友可以參考下2024-08-08
Java 8對LinkedList元素進(jìn)行排序的方法詳解
在Java中,LinkedList是一種基于鏈表的數(shù)據(jù)結(jié)構(gòu),與ArrayList相比,它在進(jìn)行插入和刪除操作時表現(xiàn)出更好的性能,然而,LinkedList的元素排序也是開發(fā)中常見的需求之一,本文介紹了Java8對LinkedList元素進(jìn)行排序的方法,需要的朋友可以參考下2024-11-11

