Spring Cache 抽象
提出问题
Spring 提供了 @Cacheable、@CachePut、@CacheEvict 等注解,让缓存逻辑从业务代码中彻底解耦。但实际项目中,很多人用了一两年,遇到缓存失效、自调用不生效、多级缓存配置、穿透/击穿/雪崩等问题时依然一脸懵。
面试官问 Spring Cache 不只是考注解用法,而是想考察:你是否理解这套注解背后的 AOP 代理机制、CacheManager 的 SPI 设计、以及如何在生产环境正确地组合缓存策略(比如本地 Cache + Redis 两级)。踩过坑和没踩过坑,回答深度差很多。
分析问题
@Cacheable 的执行流程与 key 生成
@Cacheable 执行时,Spring Cache 拦截器会先查缓存(通过 Cache 抽象),命中则直接返回,不执行方法体。核心逻辑在 CacheAspectSupport 中。
缓存的 key 默认由 SimpleKeyGenerator 生成:
- 无参数 →
SimpleKey.EMPTY(不要这么干,线上得覆盖) - 一个参数 → 直接用该参数
- 多参数 →
new SimpleKey(params...)
自定义 key 用 SpEL:
@Cacheable(value = "users", key = "#user.id + '-' + #user.type")
public User findUser(User user) { ... }踩坑点:key 默认 toString(),如果入参是复杂对象且没实现 equals()/hashCode(),每次调用都会生成不同 key,缓存形同虚设。
@CachePut 与 @CacheEvict 的协同
三个注解的职责:
| 注解 | 行为 | 典型场景 |
|---|---|---|
@Cacheable | 先查缓存,命中跳过方法 | 查询 |
@CachePut | 始终执行方法,结果写入缓存 | 更新后同步 |
@CacheEvict | 清除缓存条目 | 删除后失效 |
缓存一致性典型做法:在更新方法上用 @CachePut,在删除方法上用 @CacheEvict,查询用 @Cacheable。
注意 @CacheEvict(beforeInvocation = true):默认是方法执行成功后清除,如果方法抛异常缓存不会被清。如果业务要求无论方法成败都清,就设 beforeInvocation = true。
自调用失效:同 @Transactional 的坑
这是一个高频面试点。在一个 Bean 内部调用自己的另一个 @Cacheable 方法,缓存不会生效。原因很简单:Spring 的缓存注解靠 AOP 代理实现,内部方法调用走的是 this.xxx(),绕过了代理对象,拦截器根本没被执行。
@Service
public class UserService {
@Cacheable("users")
public User getById(Long id) { ... }
// 自调用:@Cacheable 不会生效
public User getByIdWithCache(Long id) {
return getById(id); // this.getById(),不走代理
}
}解法:要么注入自己(@Autowired UserService self 然后调 self.getById()),要么把缓存方法抽到另一个 Bean 里,要么用 AopContext.currentProxy() 配合 @EnableAspectJAutoProxy(exposeProxy = true)。和 @Transactional 的解法完全一致,因为底层都是同一个 AOP 机制。
多级缓存与 CacheManager 扩展
Spring Cache 的 CacheManager 是一个 SPI 接口,默认实现有 ConcurrentMapCacheManager、RedisCacheManager、CaffeineCacheManager 等。如果要实现本地缓存 + Redis 两级缓存,可以自定义 CacheManager 或使用 CompositeCacheManager。
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
CaffeineCacheManager caffeine = new CaffeineCacheManager();
caffeine.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES));
RedisCacheManager redis = RedisCacheManager.builder(factory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)))
.build();
CompositeCacheManager composite = new CompositeCacheManager(caffeine, redis);
composite.setFallbackToNoOpCache(false);
return composite;
}CompositeCacheManager 会按顺序查找每个 CacheManager,找到第一个匹配的 name 就返回。注意它的顺序:本地缓存在前,Redis 在后,这样本地命中就不用查 Redis。
总结
Spring Cache 的核心是对 AOP 拦截 + CacheManager SPI 的封装。日常使用中,有几个关键点需要记住:
- 自调用失效是 AOP 代理的基础问题,和
@Transactional同源,解法也一致 - key 生成不要依赖默认策略,用 SpEL 显式指定,尤其注意
toString()的坑 @Cacheable+@CachePut+@CacheEvict组合使用,保证缓存与数据库一致- 多级缓存考虑
CompositeCacheManager,本地缓存 Caffeine + 远程 Redis - 缓存穿透防范:
@Cacheable(sync = true)或自己实现cacheNull策略 - 生产环境务必监控缓存命中率,Metrics 可通过 Micrometer 导出
参考
参考:Spring Framework 文档 — Cache Abstraction 参考:Spring Boot 文档 — Caching 参考:Spring Cache 源码 —
CacheAspectSupport、CacheInterceptor、SimpleKeyGenerator