Skip to content

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:

java
@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(),绕过了代理对象,拦截器根本没被执行。

java
@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 接口,默认实现有 ConcurrentMapCacheManagerRedisCacheManagerCaffeineCacheManager 等。如果要实现本地缓存 + Redis 两级缓存,可以自定义 CacheManager 或使用 CompositeCacheManager

java
@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 源码 — CacheAspectSupportCacheInterceptorSimpleKeyGenerator

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。