SpringBoot 整合 Redis 布隆过滤器,解决缓存穿透实战
【摘要】 缓存穿透是后端开发高频问题:大量请求查询数据库不存在的数据,绕过缓存直接打满DB。布隆过滤器是一种空间高效的概率型数据结构,可以快速判断一个key一定不存在或者可能存在。本文基于SpringBoot整合Redisson自带布隆过滤器实现,提供完整可运行代码,讲解原理、测试方式以及误判问题的处理方案,对比缓存穿透几种解决方案的优劣,适合接入项目做缓存防护。
前言
在缓存架构中,缓存穿透、缓存击穿、缓存雪崩是经典三大问题。其中缓存穿透指:请求查询一个数据库里根本不存在的数据,缓存查不到,每次请求都落到数据库。如果被恶意批量请求,很容易压垮数据库。
常见解决方案:缓存空值、布隆过滤器。缓存空值简单,但如果不存在的key非常多,会占用大量Redis内存;布隆过滤器只存储特征,内存占用极低,适合海量key过滤场景。
重点:布隆过滤器不存在误报,存在误判。它只能保证:如果返回不存在,则一定不存在;返回存在,有可能不存在。
一、引入依赖
这里使用Redisson,内置封装好的布隆过滤器,不用自己手写位图逻辑。
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.20.0</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.yml redis配置
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
二、布隆过滤器初始化工具类
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
@Component
public class BloomFilterUtil {
@Resource
private RedissonClient redissonClient;
private RBloomFilter<Long> userBloomFilter;
// 布隆过滤器名称
private static final String BLOOM_USER = "bloom:user";
@PostConstruct
public void init() {
userBloomFilter = redissonClient.getBloomFilter(BLOOM_USER);
// 初始化:预期插入数量100万,误判率0.01(1%)
boolean tryInit = userBloomFilter.tryInit(1000000L, 0.01);
if (tryInit) {
System.out.println("用户布隆过滤器初始化成功");
} else {
System.out.println("布隆过滤器已经存在,复用旧实例");
}
}
// 添加元素
public void addUserId(Long userId) {
userBloomFilter.add(userId);
}
// 判断是否可能存在
public boolean mightContain(Long userId) {
return userBloomFilter.contains(userId);
}
}
三、业务接口,增加布隆过滤器拦截逻辑
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private BloomFilterUtil bloomFilterUtil;
@Resource
private UserService userService;
@GetMapping("/get")
public String getUser(@RequestParam Long userId) {
// 第一步:布隆过滤器判断
boolean exist = bloomFilterUtil.mightContain(userId);
if (!exist) {
log.warn("布隆过滤器拦截,该用户id不存在:{}", userId);
return "用户不存在";
}
// 布隆放行,再查缓存+数据库
return userService.getUserInfo(userId);
}
}
四、业务Service示意
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserService {
@Resource
private BloomFilterUtil bloomFilterUtil;
// 新增用户时,同步把id加入布隆过滤器
public void addUser(Long userId) {
// 保存DB逻辑...
bloomFilterUtil.addUserId(userId);
}
public String getUserInfo(Long userId) {
// 先查Redis缓存,缓存没有再查MySQL
return "用户信息";
}
}
五、核心问题与坑点
- 布隆过滤器不能删除元素
传统布隆过滤器不支持删除,如果用户数据删除,直接从DB删掉,但布隆里面标记还在,会出现持续误判。解决方案:
- 使用计数布隆过滤器(Redisson也支持);
- 定时重建布隆过滤器,全量从数据库重新加载;
- 业务上很少做物理删除,使用逻辑删除。
- 误判率设置
误判率越低,需要的位图空间和哈希函数数量越多,CPU开销变大。常规业务0.01或者0.001足够。 - 预热问题
项目刚启动时布隆是空的,所有请求都会放行。项目启动后需要异步任务从数据库全量加载已有数据到布隆过滤器。
// 启动后异步预热示例
@PostConstruct
public void loadAllData() {
// 线程池异步,查询所有有效userId,循环add到布隆
}
- 不适合频繁新增海量数据并且会删除的场景
六、缓存穿透方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 缓存空值 | 实现最简单,无额外组件 | 不存在key很多时占用大量Redis内存 |
| 布隆过滤器 | 内存占用极小,适合海量key | 有概率误判、无法删除、需要预热 |
七、生产优化建议
- 布隆过滤器单独一个Redis节点,避免和业务缓存相互影响;
- 做好监控:记录被布隆拦截的请求数量,观察拦截量;
- 布隆只是第一层防护,不能完全替代缓存+数据库校验;
- 如果是分布式多实例,共用同一个Redis上的布隆实例,数据统一。
结语
布隆过滤器是一道前置屏障,在高并发、海量查询场景下用来防御缓存穿透效果很好。很多同学直接照搬网上代码,忽略了它不能删除、需要预热、存在误判这些特性,上线踩坑。理解它的适用边界,搭配缓存空值兜底,才能形成稳定的穿透防护方案。本文基于Redisson实现,代码开箱即用,可以快速集成到现有SpringBoot项目。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)