缓存穿透本质是非法/不存在key频繁查询导致请求直达DB:需用AOP切面拦截并校验参数类型,统一调用key生成工具,空值缓存设短TTL(120秒)并配合布隆过滤器,Redis写入必须异步且仅在DB返回null时执行。

缓存穿透的本质是查不到 + 频繁打到DB
缓存穿透不是Redis配置问题,而是业务逻辑漏洞:用户用大量非法/不存在的key(比如 user:id:999999999)反复请求,缓存没命中,直接穿透到数据库。DB扛不住就拖垮整个服务。AOP切面在这里不解决“Redis怎么配”,而是拦截请求、识别风险key、自动兜底响应——核心是「提前拦截+统一填充」。
@Around 切面必须捕获真实参数并校验key生成逻辑
很多人直接在切面里硬写 "user:" + id 拼key,结果和业务层实际用的key不一致,导致布隆过滤器或空值缓存失效。正确做法是让切面从目标方法参数中提取原始ID,再调用统一的key生成工具类(比如 CacheKeyBuilder.buildUserKey(id))。
常见错误现象:NullPointerException 或切面生效但缓存始终不命中,往往是因为参数类型没匹配上(如方法接收 Long,切面却按 String 取值)。
- 用
ProceedingJoinPoint的getArgs()拿参数,别依赖注解位置 - 对参数做非空和类型校验,
id == null || !(id instanceof Long)就跳过缓存逻辑 - key生成必须和DAO层完全一致,建议抽成
static工具方法,避免两处维护
空值缓存 + 过期时间要分开控制,不能只设5分钟
空值缓存(SET user:id:123 "" EX 60)防穿透有效,但若所有空值都设固定短过期(比如60秒),高并发下仍可能在过期瞬间被击穿。更稳妥的是:空值用短TTL(60–120秒),同时加一个长周期的布隆过滤器(bloom:user:exists)做前置判断。
实操建议:
- 空值写入时用
set(key, "", "EX", "120"),别用setex命令(Jedis/Lettuce客户端行为不一致) - 如果业务允许少量误判,布隆过滤器用
guava BloomFilter本地内存即可;跨服务需用RedisBloom模块或自建bitmap - 避免空值覆盖正常数据:检查返回值是否为
null或空对象,而不是只看DB查询结果是否为空集合
切面里调Redis必须用异步非阻塞方式写空值
同步写空值会拖慢主流程——尤其在QPS高的接口里,一次 set 延迟叠加后,吞吐量断崖下跌。Lettuce原生支持异步命令,Jedis则需包装成 CompletableFuture 手动提交线程池。
关键点:
- 不要在
proceed()前写空值,要在捕获到DB返回null后,用redisTemplate.opsForValue().setIfAbsent(...).thenAccept(...)异步执行 - 异步失败不重试,记录warn日志即可;空值漏写最多导致单次穿透,不影响一致性
- 切面内禁止调用
Thread.sleep()或任何阻塞操作,否则整个Tomcat线程池会被吃光
最易被忽略的是异常分支:当DB抛出 SQLException 或超时,切面必须明确区分「查无结果」和「查询失败」,后者绝不能写空值缓存。


















