Skip to content

Spring 中的 @Cacheable 和 @CacheEvict 注解的作用是什么? ​

  • 标签:后端、Spring

🏛 业务架构师 ​

@Cacheable 和 @CacheEvict 是 Spring Cache 抽象的声明式缓存注解:

  • @Cacheable:方法执行前先查缓存(按 key),命中直接返回、不执行方法;未命中执行后把结果放入缓存。省掉的是重复的计算或数据库查询;
  • @CacheEvict:把指定 key/allEntries 的缓存删掉,保证数据变更后缓存不返回旧值。配套的还有 @CachePut(执行且更新缓存)、@Caching(组合)。

业务架构视角,缓存注解好用但有几个必须治理的风险点:

  1. 缓存与业务一致性:更新 DB 后失效缓存的时序(先更库再删缓存)、失败补偿,@CacheEvict 只是工具,一致性策略要设计。有更新操作的业务,缓存必须有失效路径,否则注解方便反而是事故加速器;
  2. 过期与击穿:注解本身不带 TTL(取决于 CacheManager,Caffeine/Redis 配置),热点 key 失效瞬间的并发回源要考虑;
  3. 什么该缓存:读多写少、可容忍短暂陈旧的数据(字典、商品详情、配置)适合;余额、库存这类强一致数据不进声明式缓存,要走专门的缓存+DB 一致性设计;
  4. 团队规范:缓存名统一前缀分租户、key 用 SpEL 显式声明(含业务主键),便于容量评估和批量清理。

一句话:注解降低了缓存使用门槛,但"能不能缓存"的业务判断不能被门槛降低所掩盖。

🏗 系统架构师 ​

作用:声明式缓存抽象——@Cacheable 先查缓存命中即返回、未命中执行方法回填;@CacheEvict 失效;另有 @CachePut(执行并更新)、@Caching 组合。底层由 CacheManager 适配 Caffeine/Redis。

架构师必须讲一致性与可靠性:

  1. 失效策略优先 TTL:@CacheEvict 只删单键,写库与删缓存的非原子性无法根除——「先更库后删缓存加延迟双删」是工程折中;缓存一致性只有程度,没有绝对;
  2. 三大风险防护:穿透(空值缓存/布隆过滤器)、击穿(热点 key 用 sync=true 单飞回源)、雪崩(TTL 加随机抖动、多级缓存);
  3. 命中率是生命线:读多写少(配置、字典、画像片段)才值得缓存;Caffeine 本地加 Redis 分布式的两级结构是标配——本地扛读、Redis 管一致性,命中率目标按业务定(九成以上才算健康);
  4. 自调用不走代理,失效问题同 AOP 原理。

💻 全栈开发专家 ​

声明式缓存:一个注解把"查缓存 → miss 查库 → 写缓存"全包了。

java
@Cacheable(cacheNames = "dict", key = "#type", unless = "#result == null")
public List<Dict> getByType(String type) {
    return dictMapper.selectByType(type);       // miss 时才执行
}

@CacheEvict(cacheNames = "dict", key = "#type")             // 更新逐出
public void update(String type, Dict d) { ... }

@CacheEvict(cacheNames = "dict", allEntries = true)         // 清整块
public void reload() { ... }

关键属性:key 是 SpEL(#参数名、#result);unless = "#result == null" 防缓存穿透(null 也短缓存一份)。

生产坑清单:

  1. 自调用失效:同类调用不过代理,缓存无效(和事务同根同源)
  2. key 漏维度:多租户系统没拼 tenantId 直接串数据,key 必须含租户标识
  3. 序列化:Redis 默认 JDK 序列化可读性差,配 GenericJackson2JsonRedisSerializer
  4. 一致性:先更新 DB 再 evict,强一致场景 TTL 兜底或延迟双删
  5. TTL 按 cacheName 在 RedisCacheManager 里分别配置,Spring Cache 抽象本身不带细粒度 TTL

排查"缓存没生效":开 logging.level.org.springframework.cache=DEBUG,看是否命中。