在 Spring Boot 应用中,缓存是提升接口吞吐能力的常用手段。当我们在 Service 层使用 @Cacheable 把方法结果缓存起来后,数据发生变更时就必须把旧缓存清理掉,否则就会出现查询到的数据和数据库不一致的情况。Spring 提供的 @CacheEvict 正是用来做缓存淘汰的注解,但它并不是写上就能生效,必须配合 @EnableCaching 开启缓存支持,并且依赖正确的缓存管理器与表达式配置。很多人在整合时只加了注解却忘了开启开关,或者把 allEntries 和 beforeInvocation 的含义弄混,最终导致清理动作根本没有执行。

一、@EnableCaching 的配置入口与底层原理
@EnableCaching 是一个用于开启 Spring 缓存抽象支持的注解,通常加在启动类或者独立的配置类上。它的作用是向容器中注册一系列后置处理器,其中最关键的是 CacheInterceptor 和对应的 Advisor,它们会扫描所有带了缓存相关注解的 Bean 方法,在调用前后织入缓存逻辑。如果没有这个注解,Spring 就不会创建这些代理增强,哪怕你写了 @CacheEvict 也只是一个普通注释,完全不会被识别。
在 Spring Boot 中,由于自动配置机制的存在,我们往往只需要显式标上 @EnableCaching 即可,具体的 CacheManager 会根据 classpath 中的依赖自动装配。例如引入了 Redis 起步依赖就会使用 RedisCacheManager,引入了 Caffeine 就会使用 CaffeineCacheManager。需要注意的是,如果你自定义了 CacheManager Bean,自动配置就会退让,此时必须保证该管理器支持你声明的缓存名称,否则运行时会抛出找不到缓存的异常。
下面是一段最基础的开启缓存配置示例,启动类直接组合了注解:
@SpringBootApplication
@EnableCaching
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
从原理上看,@EnableCaching 通过导入 CachingConfigurationSelector 来决策使用基于代理还是基于 AspectJ 的模式。绝大多数 Spring Boot 项目使用默认的代理模式即可,它要求被注解的方法是 public 且通过 Bean 间调用触发,自调用(同一个类内部方法互调)不会经过代理,这也是很多人遇到“注解不生效”的隐藏原因。
二、@CacheEvict 的核心参数与正确使用方式
@CacheEvict 主要用来移除缓存项,它有几个容易混淆的属性。首先是 value 或 cacheNames,指定要操作哪一个缓存空间;其次是 key,用于定位具体的缓存键,支持 SpEL 表达式;allEntries 若为 true,则会清空该缓存空间下的所有条目,而不只是匹配 key 的那一条;beforeInvocation 控制是在方法执行前还是执行后淘汰缓存,默认是 false 即方法成功返回后才清理,如果方法抛异常则不会清理。
在实际业务中,更新用户信息的操作通常这样写:先根据 id 改库,再淘汰对应缓存。如果只改了库但 key 写错,比如用了方法参数名不匹配 SpEL,就会导致旧缓存残留。推荐明确写出 key = "#id" 来保证键一致。另外,当批量操作需要清空整个缓存区域时,应当设置 allEntries = true,避免逐条计算 key 带来的性能浪费与遗漏。
以下代码展示了更新与删除场景中 @CacheEvict 的典型用法:
@Service
public class UserService {
// 更新用户,方法执行后根据 id 淘汰缓存
@CacheEvict(cacheNames = "user", key = "#user.id")
public void updateUser(User user) {
// 数据库更新逻辑
}
// 删除全部用户缓存,在方法执行前清理,防止删除失败仍留脏数据
@CacheEvict(cacheNames = "user", allEntries = true, beforeInvocation = true)
public void clearAllUserCache() {
// 批量删除逻辑
}
}
还有一个细节是 condition 与 unless 的区别:condition 在方法调用前判断是否需要执行淘汰,unless 在方法执行后根据结果判断。若错误地把本应放在 condition 中的非空校验写进 unless,可能造成空指针或无效清理。理解这些参数的触发时机,是整合不出错的关键。
三、常见整合误区与多缓存管理器下的注意事项
第一个常见误区是认为只要加了 @CacheEvict 就一定能清理掉 @Cacheable 写入的数据。实际上两者必须处于同一个 CacheManager 且缓存名称一致。如果项目里配置了多个缓存管理器,却没有在注解中通过 cacheManager 属性指定,Spring 会选用默认的那个,而 @Cacheable 可能写到了另一个管理器对应的存储里,结果自然是删了个寂寞。
第二个误区来自自调用问题。比如在 Controller 直接调同一个 Service 里的更新方法,而该方法内部又调用了带 @CacheEvict 的私有辅助方法,由于代理只拦截外部注入调用,内部调用绕过了拦截器,缓存不会被淘汰。解决办法是拆出独立的 Service Bean,或者通过注入自身实例(需注意循环依赖)来触发代理调用。
当使用 Redis 作为缓存时,还需要注意键的序列化方式。若 @Cacheable 使用了 StringRedisSerializer 而 @CacheEvict 的 key 生成策略不同,两边生成的 Redis key 字符串不匹配,淘汰同样会失效。建议在配置类中统一 RedisCacheConfiguration,明确设置键和值的序列化器,并用统一的 key 前缀规范。
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}
}
最后要提的是事务边界。若 @CacheEvict 的方法加了 @Transactional,且 beforeInvocation = false,当事务提交前方法已返回,缓存会先被清理,但随后事务回滚,数据库没变而缓存已空,下次查询会穿透到库并重新加载。这种场景下应考虑使用 beforeInvocation = true 或在事务完成阶段借助监听器同步缓存状态,才能保证最终一致。
Spring_Boot@EnableCaching@CacheEvict修改时间:2026-08-18 04:44:37