@Cacheable 默认无法防击穿,因其仅缓存结果而不控制重建过程,缓存失效时多线程会并行执行方法查库;需通过AOP切面+Redis分布式锁实现防护,关键在统一锁key与缓存key生成逻辑、显式释放锁、设置合理过期时间。

为什么 @Cacheable 默认无法防击穿
@Cacheable 在 Spring Boot 2.7 中只是缓存「结果」,不控制「重建过程」。当缓存失效后,多个线程同时发现 cache miss,会并行执行被注解的方法——也就是同时查数据库。这不是 bug,是设计使然。它没义务帮你串行化重建逻辑。
用 AOP 拦截 + Redis 分布式锁实现无侵入防护
核心思路:在方法执行前加一层切面,对同一 key 尝试获取分布式锁;只让一个线程走原逻辑,其余等待后直接读缓存。全程不改业务代码,只需加注解。
- 定义自定义注解:
@PreventCacheBreakdown,接收lockTimeout和waitTimeout参数 - 切点匹配该注解 + 方法返回值非 void
- 切面内生成唯一锁 key(如
"lock:" + cacheName + ":" + key),用StringRedisTemplate.opsForValue().setIfAbsent()尝试加锁 - 加锁失败则休眠 50ms 后重试,最多等 200ms;超时直接抛异常或 fallback
- 加锁成功后执行双重检查(double-check):再查一次缓存,命中则直接返回,避免重复重建
关键细节:锁 key 必须和缓存 key 对齐,且注意序列化一致性
很多团队踩坑在于:切面里算的锁 key 和 @Cacheable 实际生成的缓存 key 不一致。比如你用 SpEL 表达式 #id.toString(),但切面里拼的是 "shop:" + id,导致锁失效。
- 建议统一提取 key 生成逻辑到工具类,例如
CacheKeyBuilder.build("shop", id) - 如果用了
GenericJackson2JsonRedisSerializer,确保切面中所有 key 字符串都走相同编码路径,避免因空格、引号、转义导致哈希不等 - 锁过期时间必须大于方法最大执行时间,否则可能解锁后其他线程又进来——Spring Boot 2.7 默认没看门狗,得自己设
TimeUnit.SECONDS级别过期,别用毫秒
不要依赖 synchronized 或本地锁
Spring Boot 2.7 应用基本都是多实例部署,synchronized 只在单 JVM 内有效。ReentrantLock 同理。一旦服务横向扩容,击穿立刻重现。必须用 Redis 的原子操作(SETNX / setIfAbsent)做跨节点协调。
真正容易被忽略的是锁释放时机:不能只靠过期自动清理,要在 finally 块中显式调用 redisTemplate.delete(lockKey),否则异常中断时锁残留,后续请求全卡住。











