不能靠“写个通用Starter”一劳永逸解决缓存穿透,因其本质是业务逻辑漏洞;Starter仅标准化拦截、校验、空值写入、布隆预检四步,且必须与DAO层key生成逻辑完全对齐。

直接结论:不能靠“写个通用Starter”就一劳永逸解决缓存穿透——它本质是业务逻辑漏洞,Starter只负责把拦截、校验、空值写入、布隆预检这四步标准化,且必须和DAO层key生成逻辑完全对齐。
为什么不能直接封装成Starter就完事
很多人想“抽个 redis-penetration-starter”,加个注解就自动防穿透,结果上线后发现布隆过滤器不生效、空值缓存永远不命中、甚至把正常 null 返回值也当穿透处理。根本原因有三个:
- Starter 无法自动识别你 DAO 层用的是
CacheKeyBuilder.buildUserKey(id)还是硬拼的"user:" + id,一旦不一致,整个防护链就断了 - 参数类型不匹配:方法签名是
Long userId,但切面里用args[0].toString()取值,遇到 null 就抛NullPointerException - 空值写入时机错误:在
proceed()前就往 Redis 写空值,导致 DB 实际没查就提前缓存了“假空”
@Cacheable 注解不能直接复用,必须重写切面逻辑
Spring 的 @Cacheable 默认不处理 null 返回值,也不支持布隆预检,更不会区分“未命中”和“确定不存在”。要统一防控穿透,得自己写 @Around 切面,并满足以下条件:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 切点必须精确到具体 Repository 方法,比如
@Pointcut("@annotation(org.springframework.cache.annotation.Cacheable) && execution(* com.example.repo.*.findById(..))") - 用
ProceedingJoinPoint.getArgs()拿原始参数,再调用项目内已有的CacheKeyBuilder工具类生成 key,禁止硬编码 - 只在捕获到 DB 返回
null或空对象(如Optional.empty()、new User())时,才触发空值缓存逻辑 - 空值内容必须是可识别的标记,比如
"NULL"字符串或{"exists":false}JSON,不能序列化 Javanull
布隆过滤器必须跨服务共享且持久化
微服务拆分后,一个 ID 可能在用户服务、订单服务、商品服务里都被查询,布隆过滤器如果只在本地内存(Guava BloomFilter)维护,重启就丢,扩容就失效。实操建议:
- 优先用 RedisBloom 模块的
BF.RESERVE+BF.ADD,通过定时任务或 CDC 同步 DB 新增主键到bf:user:exists - 若无法装模块,退而求其次:用 Redis 的
SET存已知合法 ID 的哈希前缀(如 MD5(id).substring(0,8)),查时先SISMEMBER,误判率略高但够用 - 所有服务读布隆前,必须对原始 ID 归一化:转小写、trim 空格、截断超长值,否则
"U123 "和"u123"被当成两个 key - 不要在每次请求里 new BloomFilter,Spring Boot Starter 中应声明为
@Bean单例,初始化时从 Redis dump 加载
空值缓存必须异步写 + TTL 分级控制
同步写空值会拖慢主流程,尤其在 QPS > 1k 的接口里,一次 set 延迟叠加后吞吐量直接腰斩。正确姿势是:
- 用 Lettuce 的
asyncCommands().set(key, "NULL", SetOption.SET_IF_ABSENT, Expiration.seconds(120)),Jedis 则包装成CompletableFuture.runAsync(() -> redisTemplate.opsForValue().setIfAbsent(key, "NULL", Duration.ofSeconds(120)), pool) - 空值 TTL 设为 60–120 秒,别用固定 24 小时——几百万个
"NULL"会长期占满内存,INFO memory显示used_memory_dataset_perc > 95%就是征兆 - 避免覆盖正常数据:写空值前加
setIfAbsent,不是set;且只在 DB 明确返回 null 时写,不响应 HTTP 400 参数错误 - Redis 写入失败不能抛异常中断主流程,要记录 warn 日志并继续返回 null,否则防穿透变成制造雪崩
最易被忽略的一点:布隆过滤器的 errorRate 和 capacity 必须按实际 ID 总量预估,不是拍脑袋设 0.01 和 100w——ID 总量 500w 却只设 capacity=100w,误判率会飙升到 20% 以上,大量真实请求被误拦。

















