51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

使用 Spring Boot3.3 结合 Redisson RBloomFilter 有效应对缓存穿透问题

使用 Spring Boot3.3 结合 Redisson RBloomFilter 有效应对缓存穿透问题

在电商平台中,商品查询是最频繁的操作之一。随着商品数量的增加和用户访问量的激增,系统可能面临频繁的缓存穿透问题。缓存穿透通常是由恶意请求或用户错误输入导致的:当请求的数据不存在时,查询将直接落到数据库上,无法通过缓存系统加速响应。这不仅增加了数据库的负载,还可能导致系统崩溃。在这种情况下,布隆过滤器作为一种高效的概率型数据结构,可以通过快速判断某个数据是否存在于集合中,来有效减少无效的数据库查询,从而解决缓存穿透问题。

布隆过滤器的基本原理是在初始化时,设置一个长度为 m 的位数组,并通过 k 个哈希函数将数据映射到位数组上。当有新元素加入时,布隆过滤器会将该元素的哈希值映射到数组的 k 个位置上,并将这些位置的位设置为 1。在查询某个元素是否存在时,布隆过滤器会通过同样的 k 个哈希函数检查对应的位是否都为 1,如果有任意一位为 0,则说明该元素不存在;如果所有位都为 1,则说明该元素可能存在。

布隆过滤器的优势在于它的空间和时间效率都非常高,适合处理大规模数据的快速查询。然而,它的缺点是存在一定的误判率,即可能会错误地判断一个不存在的元素为存在。为了降低误判率,布隆过滤器的设计需要合理选择位数组的长度 m 和哈希函数的数量 k。

在电商平台中,我们可以在缓存系统之前使用布隆过滤器,对查询请求进行预筛选,以减少对数据库的直接访问。本文将通过 Spring Boot3.3 结合 Redisson 的 RBloomFilter 实现这一方案,并展示如何通过 MyBatis-Plus 实现商品数据的查询,同时结合前端展示,完整演示该技术方案的实现细节。

运行效果:

有商品

无商品

若想获取项目完整代码以及其他文章的项目源码,且在代码编写时遇到问题需要咨询交流,欢迎加入下方的知识星球。

项目结构

  • Spring Boot 3.3

  • Redisson

  • Redis

  • MyBatis-Plus

  • Thymeleaf 模板引擎

  • Bootstrap + JS 前端框架

配置项目环境

Maven 配置 (pom.xml)

pom.xml 中引入必要的依赖:

<?xml version="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&serverTimezone=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

商品表创建与数据插入

商品表 SQL DDL 语句

首先创建商品表 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 接口和实现类

首先,创建 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);
    }
}

创建 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;

}

创建 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 方法中使用 redisHostredisPort 构建 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 添加到布隆过滤器中。这确保了在系统启动时,布隆过滤器已经初始化,并且包含所有现有商品的数据。

前端展示

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 防止缓存穿透的商品查询功能。本文提供了详细的代码示例,包括前后端的实现以及数据库初始化步骤,展示了如何在实际项目中应用布隆过滤器来提高系统性能。


赞(7)
未经允许不得转载:工具盒子 » 使用 Spring Boot3.3 结合 Redisson RBloomFilter 有效应对缓存穿透问题