更多SpringBoot3内容请关注我的专栏:《SpringBoot3》
期待您的点赞👍收藏⭐评论✍
重学SpringBoot3-集成Redis(五)之布隆过滤器
- 1. 什么是布隆过滤器?
- 基本概念
- 适用场景
- 2. 使用 Redis 实现布隆过滤器
- 项目依赖
- Redis 配置
- 3. 创建布隆过滤器服务
- 4. 布隆过滤器的初始化
- 5. 使用布隆过滤器进行缓存穿透防护
- 6. 测试效果
- 6.1. 启动项目
- 6.2. 查询存在的商品
- 6.3. 查询不存在的商品
- 7. 总结
在高并发场景下,缓存是提升系统性能的重要手段。然而,常规缓存机制中,若遇到大量无效请求访问(请求的 key 不存在于缓存或数据库),就会导致 缓存穿透。为了应对这种问题,布隆过滤器 和 缓存空值 是应对缓存穿透的两大主流方案,布隆过滤器适用于大规模、复杂场景,缓存空值适用于小规模场景。布隆过滤器(Bloom Filter) 能够通过哈希算法判断一个 key 是否可能存在,减少无效请求对数据库的压力。
本篇博客将介绍如何使用 Spring Boot 3 和 Redis 实现布隆过滤器,并结合示例代码来详细讲解布隆过滤器的原理和在 Redis 中的实现方式。
1. 什么是布隆过滤器?
基本概念
布隆过滤器是一种空间效率高的 概率性数据结构,用于快速判断某个元素是否在集合中。它有以下特点:
- 内存占用小:相比传统的集合结构,布隆过滤器的内存使用更少。
- 可能存在误判:布隆过滤器只能确定某个元素“可能存在”或“绝对不存在”。但存在误判的概率可以通过调整参数降低。
- 不支持删除:布隆过滤器不支持删除已添加的元素,删除某个元素会导致误判率增加。
适用场景
布隆过滤器在以下场景中非常适用:
- 防止缓存穿透:将不存在的 key 存储在布隆过滤器中,避免大量无效请求直接查询数据库。
- 防止重复数据:在大规模数据处理中,使用布隆过滤器避免重复处理相同的数据。
2. 使用 Redis 实现布隆过滤器
Redis 提供了开箱即用的布隆过滤器功能,通过 Redis 的插件 RedisBloom,安装过程参考:Redis安装RedisBloom,我们可以非常方便地使用布隆过滤器存储和管理 key。
项目依赖
首先,在 Spring Boot 项目中引入相关依赖,可参考之前文章。需要 Redis 的支持,以及 Spring Data Redis 来实现与 Redis 的交互。
注意: Redisson 提供了对布隆过滤器的支持,具体实现会利用它的 API。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.20.0</version>
</dependency>
Redis 配置
在 application.yml
文件中配置 Redis 的连接信息,详细请参考上一章重学SpringBoot3-集成Redis(四)之Redisson,进行 Redisson 配置。
spring:
redis:
redisson:
config: |
singleServerConfig:
address: redis://1.94.26.81:6379 # Redis 连接地址,前缀为 redis://
password: redis123456 # 如果 Redis 需要密码认证,则填写密码
timeout: 3000 # 命令执行超时时间(毫秒)
配置类中初始化 Redisson 客户端:
package com.coderjia.boot310redis.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.spring.starter.RedissonProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author CoderJia
* @create 2024/10/5 下午 04:53
* @Description
**/
@Configuration
public class RedissonConfig {
@Autowired
private RedissonProperties redissonProperties;
@Bean
public RedissonClient redissonClient() throws Exception{
Config config = Config.fromYAML(redissonProperties.getConfig());
Redisson.create(config);
System.out.println("Redisson 已启动");
return Redisson.create(config);
}
}
3. 创建布隆过滤器服务
接下来,我们需要定义一个服务来管理布隆过滤器。利用 Redisson 提供的 API,可以轻松实现布隆过滤器。
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BloomFilterService {
private static final String BLOOM_FILTER_NAME = "bloomFilter";
@Autowired
private RedissonClient redissonClient;
// 初始化布隆过滤器
public void initBloomFilter(long expectedInsertions, double falseProbability) {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_NAME);
bloomFilter.tryInit(expectedInsertions, falseProbability);
}
// 添加元素到布隆过滤器中
public void addToBloomFilter(String key) {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_NAME);
bloomFilter.add(key);
}
// 检查元素是否存在于布隆过滤器中
public boolean mightContain(String key) {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_NAME);
return bloomFilter.contains(key);
}
}
代码解释
- 初始化布隆过滤器:
initBloomFilter
方法可以根据期望插入的数量和误判率初始化布隆过滤器。布隆过滤器的大小和哈希函数数量根据这些参数自动计算。 - 添加元素:
addToBloomFilter
方法向布隆过滤器中添加新的 key。 - 判断元素是否存在:
mightContain
方法用来判断某个 key 是否在布隆过滤器中。
4. 布隆过滤器的初始化
通常我们会在应用启动时初始化布隆过滤器,并将数据库中的所有 key 预先加入过滤器。
package com.coderjia.boot310redis.config;
import com.coderjia.boot310redis.bean.Product;
import com.coderjia.boot310redis.dao.ProductMapper;
import com.coderjia.boot310redis.service.BloomFilterService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author CoderJia
* @create 2024/10/6 下午 12:38
* @Description
**/
@Slf4j
@Component
public class BloomFilterInitializer implements CommandLineRunner {
@Autowired
private BloomFilterService bloomFilterService;
@Autowired
private ProductMapper productMapper;
@Override
public void run(String... args) throws Exception {
// 查询所有产品数据
List<Product> all = productMapper.findAll();
// 初始化布隆过滤器
bloomFilterService.initBloomFilter(all.size(), 0.01);
// 将所有产品的ID加入布隆过滤器
all.forEach(product -> {
bloomFilterService.addToBloomFilter(product.getId().toString());
});
log.info("初始化布隆过滤器完成,添加产品数:{}", all.size());
}
}
代码解释
- 在应用启动时,通过
CommandLineRunner
初始化布隆过滤器,并将数据库中的所有商品 ID 加入过滤器中。
5. 使用布隆过滤器进行缓存穿透防护
接下来,我们通过一个简单的示例,结合 Redis 的缓存功能和布隆过滤器,展示如何防止缓存穿透。
package com.coderjia.boot310redis.service;
import com.coderjia.boot310redis.bean.Product;
import com.coderjia.boot310redis.dao.ProductMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @author CoderJia
* @create 2024/10/6 下午 12:37
* @Description
**/
@Slf4j
@Service
public class ProductService {
@Autowired
private BloomFilterService bloomFilterService;
@Autowired
private ProductMapper productMapper;
@Cacheable(value = "product", key = "#p0")
public Product getProductById(Long id) {
// 使用布隆过滤器防止缓存穿透
if (!bloomFilterService.mightContain(id.toString())) {
throw new IllegalArgumentException("Product not found!");
}
log.info("准备查询产品信息,id:{}", id);
return productMapper.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Product not found!"));
}
}
代码解释
- 在查询商品前,首先通过布隆过滤器判断 key 是否可能存在。若布隆过滤器判断 key 不存在,则直接抛出异常,避免查询数据库。
- 如果布隆过滤器判断 key 可能存在,接着通过缓存获取商品数据。如果缓存未命中,则查询数据库。
productMapper
参考:
package com.coderjia.boot310redis.dao;
import com.coderjia.boot310redis.bean.Product;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Optional;
/**
* @author CoderJia
* @create 2024/3/16 下午 05:22
* @Description
**/
@Mapper
public interface ProductMapper {
Optional<Product> findById(Long id);
List<Product> findAll();
}
Product
也很简单:
package com.coderjia.boot310redis.bean;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author CoderJia
* @create 2024/10/6 下午 12:46
* @Description
**/
@Data
public class Product implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
private String name;
}
6. 测试效果
在你的业务逻辑中调用上面创建的 getProductById
方法。
package com.coderjia.boot310redis.demos.web;
import com.coderjia.boot310redis.bean.Product;
import com.coderjia.boot310redis.service.LockService;
import com.coderjia.boot310redis.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author CoderJia
* @create 2024/10/5 下午 05:14
* @Description
**/
@Slf4j
@RestController
public class LockController {
@Autowired
private ProductService productService;
@GetMapping("/get-product")
public Product getProduct(@RequestParam("id") Long id) {
log.info("准备产品产品信息,id:{}", id);
try {
Product product = productService.getProductById(id);
return product;
}catch (Exception e) {
log.error("获取产品信息异常,id:{}", id, e);
return null;
}
}
}
6.1. 启动项目
可以看到布隆过滤器的初始化过程,查询出所有的产品信息并添加到布隆过滤器中。
6.2. 查询存在的商品
调用 curl "http://localhost:8080/get-product?id=1"
接口:
6.3. 查询不存在的商品
调用 curl "http://localhost:8080/get-product?id=101"
接口,产品不存在布隆过滤器器中,直接报错。
7. 总结
通过结合 Spring Boot 3、Redis 和 Redisson,我们可以非常方便地实现布隆过滤器,来防止缓存穿透问题。在高并发场景下,布隆过滤器是一种有效的工具,可以降低数据库的压力,提升系统性能。布隆过滤器并不是万能的,在某些场景下会有少量误判,但结合 Redis 的强大功能,它依然是防止缓存穿透的最佳选择之一。
关键点总结
- 布隆过滤器通过空间换时间,能够快速判断元素是否存在,减少无效请求。
- Redisson 提供了开箱即用的布隆过滤器 API,大大简化了开发工作。
- 在结合缓存时,布隆过滤器可以显著减少数据库查询,提升系统性能。