目录
布隆过滤器防缓存穿透学习笔记
一、学习前置:明确核心问题与学习目标
1.1 核心业务问题
二、技术选型:布谷鸟过滤器 vs Redisson布隆过滤器(关键决策过程)
2.1 学习疑问
2.2 知识点补充:两种过滤器的核心原理
(1)布隆过滤器原理
(2)布谷鸟过滤器原理
2.3 决策过程:为什么放弃布谷鸟,选择Redisson布隆?
三、架构演进:从单场景→通用型→零侵入
3.1 阶段1:单场景最小可用落地(解决「有没有」的问题)
3.1.1 场景痛点
3.1.2 实现思路
3.1.3 学习思考
3.2 阶段2:通用型架构(解决「复用性」问题)
3.2.1 场景痛点
3.2.2 知识点补充:枚举的通用配置作用
3.2.3 实现思路
3.2.4 学习思考
3.3 阶段3:零侵入终极架构(解决「业务入侵」问题)
3.3.1 场景痛点
3.3.2 核心思路
3.3.4 实现方案(零侵入的两个核心操作)
(1)零侵入全量预热
(2)零侵入定时增量同步
四、假阳性兜底:懒加载黑名单(理解设计逻辑)
4.1 学习疑问
4.2 知识点补充:装饰器模式
4.3 核心思路
4.4 实现方案
(1)通用黑名单服务(BloomBlacklistService)
(2)通用化删除状态判断(函数式接口)
(3)向后兼容的缓存方法设计
4.5 学习结论
五、踩坑记录:常见报错与解决方案
5.1 报错1:Redis注入类型不匹配
报错信息
知识点补充:Spring Boot自动注册的两个Redis Bean
报错根因
解决方案
5.2 报错2:Redisson版本不兼容
报错信息
报错根因
解决方案
六、最终架构
6.1 最终全链路架构
6.2 全场景适配规则
布隆过滤器防缓存穿透学习笔记
本次学习围绕黑马点评,完整落地布隆过滤器防缓存穿透方案,核心是掌握「从技术选型→架构演进→落地排错」的全流程,理解每一步决策的原因、底层原理,以及如何做到「零侵入、高复用、生产可用」,现将学习过程、知识点、思考点整理如下:
一、学习前置:明确核心问题与学习目标
1.1 核心业务问题
黑马点评作为分布式项目,存在「缓存穿透」风险:大量数据库中不存在的ID请求(如恶意刷非法ID、爬虫乱试ID),会绕过缓存直接打到MySQL,导致数据库CPU打满、服务雪崩,这是分布式项目中缓存层的核心痛点。
二、技术选型:布谷鸟过滤器 vs Redisson布隆过滤器(关键决策过程)
2.1 学习疑问
一开始纠结:布谷鸟过滤器理论上比布隆过滤器更优(支持删除、空间利用率高),为什么最终放弃布谷鸟,选择Redisson布隆过滤器?
2.2 知识点补充:两种过滤器的核心原理
(1)布隆过滤器原理
布隆过滤器由「位数组+多个哈希函数」组成,核心逻辑:
-
插入数据:将数据通过多个哈希函数映射到位数组的不同位置,将对应位置置为1;
-
查询数据:同样通过多个哈希函数映射,若所有映射位置都是1,则判定「可能存在」;若有一个位置是0,则判定「绝对不存在」;
-
核心特性:绝对无假阴性(不存在的key一定被拦截),存在假阳性(存在的key可能被误判为不存在,概率可控制),不支持删除。
(2)布谷鸟过滤器原理
基于布隆过滤器优化,采用「哈希表+踢人机制」,核心逻辑:
-
插入数据:通过两个哈希函数映射两个位置,若有一个位置为空则插入;若都不为空,随机踢走一个数据,被踢走的数据重新映射插入;
-
核心特性:支持删除,空间利用率高,但存在「负载因子上限」,且有假阴性风险。
2.3 决策过程:为什么放弃布谷鸟,选择Redisson布隆?
核心原则:分布式业务场景下,「稳定、风险可控、生态适配」比「理论最优」更重要,布谷鸟的缺陷在分布式场景中是致命的:
-
缺陷1:插入可能直接失败——负载因子超过上限后,无法找到空闲位置,直接拒绝写入,导致新增业务数据无法进入过滤器,业务会出现异常(远比假阳性严重);
-
缺陷2:分布式无成熟实现——Java生态无Redisson官方稳定支持,多节点并发插入时,「踢人机制」的原子性无法保证,会出现数据错乱;
-
缺陷3:存在假阴性——重复插入+按指纹删除,可能导致「实际存在的key被判定为不存在」,破坏过滤器的核心作用;
-
缺陷4:扩容复杂——初始化后容量固定,满容后需要全量迁移数据,运维成本高。
Redisson布隆过滤器的优势:
-
优势1:绝对无假阴性——数学保证「不存在的key一定被拦截」,精准解决缓存穿透核心痛点;
-
优势2:分布式生态成熟——Redisson原生实现,完美兼容Redis单机/集群/哨兵,是Java企业级项目的标准方案;
-
优势3:性能稳定——插入/查询均为O(1),高并发下无性能抖动;
-
优势4:风险可控——假阳性可通过黑名单兜底,且误判率可通过配置(容量、哈希函数个数)控制在0.1%以下;
-
优势5:极简落地——无需额外部署Redis插件,引入依赖即可使用。
三、架构演进:从单场景→通用型→零侵入
架构演进的核心思路:「最小可用→通用复用→零侵入解耦」,遵循开闭原则(新增功能不修改原有代码)、单一职责原则(业务层与布隆层彻底分离),每一步演进都对应具体的业务痛点。
3.1 阶段1:单场景最小可用落地(解决「有没有」的问题)
3.1.1 场景痛点
初期只需要防护「商户查询」场景,先验证布隆过滤器的核心拦截能力,无需考虑复用性,优先跑通核心流程。
3.1.2 实现思路
聚焦商户场景,做最小可用实现,核心完成3件事:
-
初始化:项目启动时,通过@PostConstruct注解+tryInit()幂等方法,初始化Redisson布隆过滤器(避免重复初始化);
-
核心API:封装addShopId()(新增商户ID)、containsShopId()(判断商户ID是否存在);
-
接口接入:在商户查询Controller层添加拦截逻辑,布隆判定不存在则直接返回,不碰缓存和数据库。
3.1.3 学习思考
这一步的核心是「验证可行性」,不追求完美,先解决缓存穿透的核心问题,同时熟悉Redisson布隆的基本API,为后续通用化打下基础。
3.2 阶段2:通用型架构(解决「复用性」问题)
3.2.1 场景痛点
业务需要覆盖商户、优惠券、秒杀、笔记等多个场景,每个场景的「布隆容量、误判率」需求不同(比如秒杀场景数据量小、误判率要求高;商户场景数据量大、误判率可放宽),单场景代码复用性极差,新增场景需要重复写初始化、API封装代码,维护成本高。
3.2.2 知识点补充:枚举的通用配置作用
用枚举管理场景配置,可实现「配置与核心逻辑解耦」,每个枚举项对应一个业务场景,包含3个核心配置:
-
redisKey:布隆过滤器在Redis中的存储键(避免不同场景冲突);
-
expectedSize:预期容量(场景的最大数据量,影响布隆的误判率和空间占用);
-
falseProbability:误判率(如0.001,即0.1%,误判率越低,需要的位数组越大)。
3.2.3 实现思路
-
场景枚举:定义BloomSceneEnum,包含所有业务场景的配置(商户、优惠券、秒杀、笔记);
-
通用服务:封装BloomFilterService,提供init()(初始化)、add()(新增数据)、contains()(判断存在)通用API,通过场景枚举区分不同业务,不绑定任何具体业务逻辑;
-
自动初始化:项目启动时,遍历BloomSceneEnum,自动初始化所有场景的布隆过滤器,无需手动维护。
3.2.4 学习思考
通用化设计的核心是「提取共性、隔离差异」:所有场景的布隆操作(初始化、新增、查询)是共性,场景配置(容量、误判率、RedisKey)是差异,用枚举管理差异,核心服务处理共性,实现「新增场景只加枚举项,不改核心代码」。
3.3 阶段3:零侵入终极架构(解决「业务入侵」问题)
3.3.1 场景痛点
之前的实现需要在业务代码中加入布隆相关逻辑(比如新增商户时调用add()方法、在业务Service中新增预热专用的查询方法),存在两个严重问题:
-
入侵业务根逻辑:业务层需要感知布隆的存在,违背单一职责原则(业务层只负责业务,布隆层只负责防穿透);
-
维护成本高:新增业务、修改业务时,需要同步修改布隆相关代码,容易出错。
3.3.2 核心思路
彻底解耦:布隆层主动拉取业务数据,不依赖业务Service的自定义方法,只利用MyBatis-Plus的原生能力,实现「业务层完全无感知」。
3.3.4 实现方案(零侵入的两个核心操作)
(1)零侵入全量预热
作用:项目启动时,将所有现有有效数据的ID写入布隆过滤器,避免启动初期缓存穿透。
-
实现逻辑:独立的BloomPreheatService,注入业务Service(ShopServiceImpl、BlogServiceImpl等),项目启动后异步执行预热;
-
分布式安全:用Redisson全局分布式锁,保证集群多节点部署时,只执行一次预热(避免重复操作,浪费资源);
-
零侵入保证:不新增任何业务Service的自定义方法,直接用MP的lambdaQuery()查询有效数据的ID,批量写入布隆。
(2)零侵入定时增量同步
作用:实时同步新增数据的ID到布隆过滤器,避免新增数据无法被拦截(比如新增商户、新增笔记)。
-
实现逻辑:独立的BloomIncrementSyncTask,分场景配置定时任务;
-
增量查询:通过「id > 上一次同步的最大ID」,只查询新增数据,避免全表扫描;
-
分布式安全:每个场景加独立的分布式锁,防止集群重复执行;
-
零侵入保证:不修改任何业务接口,定时任务主动拉取新增数据,业务层完全无感知。
四、假阳性兜底:懒加载黑名单(理解设计逻辑)
4.1 学习疑问
布隆过滤器存在假阳性(不存在的key被判定为存在),会导致极少数无效请求打到DB,如何兜底?同时要求:黑名单懒加载(不提前预热)、不修改原有缓存方法、向后兼容(部分场景不需要布隆)。
4.2 知识点补充:装饰器模式
装饰器模式:在不修改原有方法的前提下,为方法添加额外功能(比如在原有缓存方法前后,添加布隆+黑名单的校验逻辑),完美实现向后兼容。
4.3 核心思路
-
前置拦截:布隆判定→黑名单二次校验,只有都通过,才进入原有缓存逻辑;
-
懒加载写入:只有当DB查询返回空/已删除数据时,才将ID写入黑名单(不提前预热,节省Redis空间);
-
向后兼容:新增带布隆的缓存方法,内部直接调用原有缓存方法,原方法完全不动,按需选择使用。
4.4 实现方案
(1)通用黑名单服务(BloomBlacklistService)
-
核心能力:判断ID是否在黑名单(isBlacklisted)、将ID加入黑名单(addToBlacklist);
-
优化点:黑名单带30天TTL(避免无限膨胀),用Redis Set存储(查询高效、去重)。
(2)通用化删除状态判断(函数式接口)
问题:不同业务的「删除状态字段」可能不同(比如商户/笔记是status=0,用户是isDeleted=1),如何让黑名单服务通用?
解决方案:用Java 8的Function<R, Boolean>函数式接口,调用时传入「判断数据是否已删除」的规则,比如:
-
商户:s → s.getStatus() == 0(status=0表示已删除);
-
用户:u → u.getIsDeleted() == 1(isDeleted=1表示已删除)。
知识点补充:函数式接口的类型推导——编译器通过「数据库查询方法的返回值类型」,自动推导lambda表达式中参数的类型(比如shopService::getById返回Shop,所以s就是Shop类型),无需手动指定。
(3)向后兼容的缓存方法设计
-
原方法(queryWithLogicalExpire):完全不动,供不需要布隆的场景(比如优惠券,已用Lua脚本防穿透)继续调用;
-
新方法(queryWithBloomAndLogicalExpire):内部直接调用原方法,在前后添加布隆+黑名单逻辑,供需要布隆的场景(商户、笔记)调用;
-
优势:原有业务调用不受影响,新增业务按需选择,零冲突、零修改。
4.5 学习结论
假阳性兜底的核心是「懒加载黑名单+装饰器模式」:既解决了假阳性问题,又保证了向后兼容,不入侵原有代码,同时通过函数式接口实现全场景通用,符合高复用、低耦合的设计原则。
五、踩坑记录:常见报错与解决方案
落地过程中遇到两个典型报错,核心都是「框架版本/配置不兼容」,整理报错根因、知识点、解决方案,帮助后续避坑。
5.1 报错1:Redis注入类型不匹配
报错信息
Bean named 'redisTemplate' is expected to be of type 'org.springframework.data.redis.core.StringRedisTemplate' but was actually of type 'org.springframework.data.redis.core.RedisTemplate'
知识点补充:Spring Boot自动注册的两个Redis Bean
-
redisTemplate:默认Bean,类型是RedisTemplate<Object, Object>,用于存储对象;
-
stringRedisTemplate:专门用于存储字符串的Bean,类型是StringRedisTemplate,继承自RedisTemplate,适配字符串操作(如Redis Set、String类型缓存)。
补充:@Resource注解默认「按名称注入」,@Autowired默认「按类型注入」。
报错根因
代码中用@Resource注入StringRedisTemplate,但变量名是redisTemplate,Spring会按名称找名为redisTemplate的Bean(类型是RedisTemplate<Object, Object>),导致类型不匹配。
解决方案
-
将变量名“redisTemplate”更改为“stringRedisTemplate”
5.2 报错2:Redisson版本不兼容
报错信息
java.lang.NoSuchMethodError: 'org.redisson.api.RedissonReactiveClient org.redisson.api.RedissonClient.reactive()'
报错根因
黑马点评原版是Spring Boot 2.3.12.RELEASE,对应的Spring Data Redis版本是2.3.x,但引入了适配Spring Boot 3.x的高版本Redisson(3.26.0),高版本Redisson包含响应式编程(Reactive)API,而低版本Spring Data Redis无对应依赖,导致方法找不到。
解决方案
降级Redisson版本,适配Spring Boot 2.3.x:
-
替换Maven依赖,使用redisson-spring-data-23(适配Spring Data Redis 2.3)+ redisson 3.17.0版本;
-
刷新Maven依赖,重启项目,报错消失。
六、最终架构
6.1 最终全链路架构
请求 → 布隆过滤器拦截(不存在直接返回)→ 黑名单二次校验(已删除直接返回)→ 原有逻辑过期缓存逻辑(异步重建+分布式锁)→ DB查询 → 数据不存在/已删除→懒加载写入黑名单 → 返回结果
6.2 全场景适配规则
-
商户/笔记场景:调用queryWithBloomAndLogicalExpire,用布隆+黑名单防穿透;
-
优惠券场景:调用原有queryWithLogicalExpire,沿用Lua脚本防穿透,完全不动;
-
其他场景:按需选择,零冲突、零修改。





