使用 Spring Boot3.3 結合 Redisson RBloomFilter 有效應對緩存穿透問題
在電商平臺中,商品查詢是最頻繁的操作之一。隨著商品數量的增加和用戶訪問量的激增,系統(tǒng)可能面臨頻繁的緩存穿透問題。緩存穿透通常是由惡意請求或用戶錯誤輸入導致的:當請求的數據不存在時,查詢將直接落到數據庫上,無法通過緩存系統(tǒng)加速響應。這不僅增加了數據庫的負載,還可能導致系統(tǒng)崩潰。在這種情況下,布隆過濾器作為一種高效的概率型數據結構,可以通過快速判斷某個數據是否存在于集合中,來有效減少無效的數據庫查詢,從而解決緩存穿透問題。
布隆過濾器的基本原理是在初始化時,設置一個長度為 m 的位數組,并通過 k 個哈希函數將數據映射到位數組上。當有新元素加入時,布隆過濾器會將該元素的哈希值映射到數組的 k 個位置上,并將這些位置的位設置為 1。在查詢某個元素是否存在時,布隆過濾器會通過同樣的 k 個哈希函數檢查對應的位是否都為 1,如果有任意一位為 0,則說明該元素不存在;如果所有位都為 1,則說明該元素可能存在。
布隆過濾器的優(yōu)勢在于它的空間和時間效率都非常高,適合處理大規(guī)模數據的快速查詢。然而,它的缺點是存在一定的誤判率,即可能會錯誤地判斷一個不存在的元素為存在。為了降低誤判率,布隆過濾器的設計需要合理選擇位數組的長度 m 和哈希函數的數量 k。
在電商平臺中,我們可以在緩存系統(tǒng)之前使用布隆過濾器,對查詢請求進行預篩選,以減少對數據庫的直接訪問。本文將通過 Spring Boot3.3 結合 Redisson 的 RBloomFilter 實現這一方案,并展示如何通過 MyBatis-Plus 實現商品數據的查詢,同時結合前端展示,完整演示該技術方案的實現細節(jié)。
運行效果:
有商品
圖片
無商品
圖片
若想獲取項目完整代碼以及其他文章的項目源碼,且在代碼編寫時遇到問題需要咨詢交流,歡迎加入下方的知識星球。
項目結構
- Spring Boot 3.3
- Redisson
- Redis
- MyBatis-Plus
- Thymeleaf 模板引擎
- Bootstrap + JS 前端框架
配置項目環(huán)境
Maven 配置 (pom.xml)
在 pom.xml 中引入必要的依賴:
<?xml versinotallow="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>bloomfilter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>bloomfilter</name>
<description>BloomFilter Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
<bootstrap.version>5.3.0</bootstrap.version>
<jquery.version>3.6.0</jquery.version>
<mybatis-spring.version>3.0.3</mybatis-spring.version>
<mybatis-plus-boot-starter.version>3.5.7</mybatis-plus-boot-starter.version>
<redisson.version>3.35.0</redisson.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus 依賴 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<!-- MySQL 驅動 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Bootstrap 和 JS -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>${bootstrap.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>${jquery.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Yaml 配置 (application.yml)
在 application.yml 中配置 Redis、Redisson 以及 MyBatis-Plus 的相關信息:
spring:
datasource:
url: jdbc:mysql://localhost:3306/sensitive?useSSL=false&serverTimeznotallow=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: localhost
port: 6379
password: 123456
timeout: 60000
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
thymeleaf:
cache: false
server:
port: 8080
商品表創(chuàng)建與數據插入
商品表 SQL DDL 語句
首先創(chuàng)建商品表 product,包含商品的基本信息:
CREATE TABLE product (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL
);
插入商品數據的 SQL 語句
插入20條示例數據到 product 表中:
INSERT INTO product (name, description, price, stock) VALUES
('商品1', '這是商品1的描述', 99.99, 100),
('商品2', '這是商品2的描述', 199.99, 50),
('商品3', '這是商品3的描述', 299.99, 150),
('商品4', '這是商品4的描述', 399.99, 200),
('商品5', '這是商品5的描述', 499.99, 10),
('商品6', '這是商品6的描述', 599.99, 5),
('商品7', '這是商品7的描述', 699.99, 300),
('商品8', '這是商品8的描述', 799.99, 400),
('商品9', '這是商品9的描述', 899.99, 500),
('商品10', '這是商品10的描述', 999.99, 600),
('商品11', '這是商品11的描述', 1099.99, 700),
('商品12', '這是商品12的描述', 1199.99, 800),
('商品13', '這是商品13的描述', 1299.99, 900),
('商品14', '這是商品14的描述', 1399.99, 1000),
('商品15', '這是商品15的描述', 1499.99, 1100),
('商品16', '這是商品16的描述', 1599.99, 1200),
('商品17', '這是商品17的描述', 1699.99, 1300),
('商品18', '這是商品18的描述', 1799.99, 1400),
('商品19', '這是商品19的描述', 1899.99, 1500),
('商品20', '這是商品20的描述', 1999.99, 1600);
實現商品查詢功能
商品實體類 (Product)
package com.icoderoad.bloomfilter.entity;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@TableName("product")
@Data
public class Product {
@TableId
private Long id;
private String name;
private String description;
private BigDecimal price;
private Integer stock;
}
商品Mapper接口 (ProductMapper)
package com.icoderoad.bloomfilter.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.icoderoad.bloomfilter.entity.Product;
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
商品服務接口 (ProductService)
package com.icoderoad.bloomfilter.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.icoderoad.bloomfilter.entity.Product;
public interface ProductService extends IService<Product> {
public Product getProductById(Long id);
}
商品服務實現類 (ProductServiceImpl)
package com.icoderoad.bloomfilter.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.icoderoad.bloomfilter.entity.Product;
import com.icoderoad.bloomfilter.mapper.ProductMapper;
import com.icoderoad.bloomfilter.service.BloomFilterService;
import com.icoderoad.bloomfilter.service.ProductService;
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private BloomFilterService bloomFilterService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Product getProductById(Long id) {
String productId = id.toString();
// 使用布隆過濾器判斷商品是否存在
if (!bloomFilterService.mightContain(productId)) {
return null;
}
// 從 Redis 緩存中獲取數據
Product product = (Product) redisTemplate.opsForValue().get(productId);
if (product == null) {
// 如果緩存中沒有,從數據庫查詢
product = productMapper.selectById(id);
if (product != null) {
// 將數據放入緩存,并添加到布隆過濾器中
redisTemplate.opsForValue().set(productId, product);
bloomFilterService.addProductToBloomFilter(id);
}
}
return product;
}
}
初始化布隆過濾器
BloomFilterService 接口和實現類
首先,創(chuàng)建 BloomFilterService 接口及其實現類,用于封裝布隆過濾器的操作:
package com.icoderoad.bloomfilter.service;
public interface BloomFilterService {
void addProductToBloomFilter(Long id);
boolean mightContain(String id);
}
實現類:
package com.icoderoad.bloomfilter.service.impl;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.icoderoad.bloomfilter.service.BloomFilterService;
import jakarta.annotation.PostConstruct;
@Service
public class BloomFilterServiceImpl implements BloomFilterService {
@Autowired
private RedissonClient redissonClient;
private RBloomFilter<String> bloomFilter;
// 初始化布隆過濾器
@PostConstruct
public void init() {
if( redissonClient!=null ) {
this.bloomFilter = redissonClient.getBloomFilter("productBloomFilter");
// 初始化布隆過濾器的大小和誤判率
this.bloomFilter.tryInit(1000000L, 0.01);
}
}
@Override
public void addProductToBloomFilter(Long id) {
bloomFilter.add(id.toString());
}
@Override
public boolean mightContain(String id) {
return bloomFilter.contains(id);
}
}
創(chuàng)建 RedissonConfig 配置類
屬性類 RedisProperties:
package com.icoderoad.bloomfilter.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Data;
@Configuration
@ConfigurationProperties(prefix = "spring.data.redis")
@Data
public class RedisProperties {
private String host;
private int port;
private String password;
private int timeout;
private int database;
}
創(chuàng)建 RedissonConfig 配置類,從 application.yml 中讀取 Redis 配置:
package com.icoderoad.bloomfilter.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedissonConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean("redissonClient")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort())
.setPassword(redisProperties.getPassword())
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(5)
.setTimeout(redisProperties.getTimeout())
.setDatabase(redisProperties.getDatabase());
return Redisson.create(config);
}
}
說明
- @Value 注解用于從 application.yml 中讀取配置屬性。
- setAddress 方法中使用 redisHost 和 redisPort 構建 Redis 地址。
- 其他 Redis 配置如密碼、連接超時、數據庫索引等,也從配置文件中讀取。
在啟動時初始化 BloomFilter (ApplicationRunner)
然后,在項目啟動時將商品數據添加到 BloomFilter 中:
package com.icoderoad.bloomfilter.init;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import com.icoderoad.bloomfilter.entity.Product;
import com.icoderoad.bloomfilter.mapper.ProductMapper;
import com.icoderoad.bloomfilter.service.BloomFilterService;
@Component
public class BloomFilterInitializer {
@Autowired
private ProductMapper productMapper;
@Autowired
private BloomFilterService bloomFilterService;
@Bean
public ApplicationRunner initializeBloomFilter() {
return args -> {
// 從數據庫中獲取所有商品
List<Product> products = productMapper.selectList(null);
// 將每個商品的ID添加到布隆過濾器中
for (Product product : products) {
bloomFilterService.addProductToBloomFilter(product.getId());
}
};
}
}
這個 ApplicationRunner 會在 Spring Boot 應用啟動時運行,遍歷數據庫中的所有商品,并將它們的 ID 添加到布隆過濾器中。這確保了在系統(tǒng)啟動時,布隆過濾器已經初始化,并且包含所有現有商品的數據。
前端展示
HTML 頁面 (Thymeleaf 模板)
使用 Thymeleaf 結合 Bootstrap 實現前端頁面,允許用戶輸入商品ID并查詢商品信息:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品查詢</title>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h1>商品查詢</h1>
<form id="searchForm" th:action="@{/product/search}" method="get">
<div class="form-group">
<label for="productId">商品ID:</label>
<input type="text" class="form-control" id="productId" name="productId" required>
</div>
<button type="submit" class="btn btn-primary">查詢</button>
</form>
<div id="productResult" th:if="${product != null}">
<h2>商品信息</h2>
<p>ID: <span th:text="${product.id}"></span></p>
<p>名稱: <span th:text="${product.name}"></span></p>
<p>描述: <span th:text="${product.description}"></span></p>
<p>價格: <span th:text="${product.price}"></span></p>
<p>庫存: <span th:text="${product.stock}"></span></p>
</div>
</div>
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/bootstrap/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Controller 實現
package com.icoderoad.bloomfilter.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.icoderoad.bloomfilter.entity.Product;
import com.icoderoad.bloomfilter.service.ProductService;
@Controller
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/")
public String index(Model model) {
return "index";
}
@GetMapping("/product/search")
public String searchProduct(@RequestParam("productId") Long productId, Model model) {
Product product = productService.getProductById(productId);
model.addAttribute("product", product);
return "index";
}
}
結論
通過結合 Spring Boot3.3、Redisson 和 MyBatis-Plus,實現了使用 RBloomFilter 防止緩存穿透的商品查詢功能。本文提供了詳細的代碼示例,包括前后端的實現以及數據庫初始化步驟,展示了如何在實際項目中應用布隆過濾器來提高系統(tǒng)性能。
今天就講到這里,如果有問題需要咨詢,大家可以直接留言或掃下方二維碼來知識星球找我,我們會盡力為你解答。