导读:本期聚焦于马来西亚程序员创作的《Spring Boot 中 @CacheEvict 注解如何正确清除缓存?常见用法与避坑指南》,敬请观看详情。缓存用得好能提升接口响应速度,用不好却会带来脏数据问题。@CacheEvict 是 Spring Cache 提供的缓存清除注解,它负责在下发数据变更操作时把对应缓存删掉,保证下次查询拿到的是最新数据。本文围绕 @CacheEvict 的核心属性展开,讲解 allEntries 与 key 的配合方式、beforeInvocation 参数对异常场景的影响,以及在多缓存管理器、自定义 KeyGenerator 情况下的注意事项。同时对比 @CacheEvict 与 @CachePut 的使用边界,给出整合 Redis 作为缓存实现时的完整配置示例和常见踩坑点,帮助你构建一套可靠的缓存更新方案。

在 Spring Boot 项目里引入缓存后,很多团队遇到的第一个问题不是怎么把数据放进缓存,而是怎么在数据变更时把旧缓存正确地清掉。如果一个用户更新了自己的昵称,但缓存里还是旧昵称,这类脏数据问题会让用户直接质疑系统的可靠性。Spring Cache 抽象层提供的 @CacheEvict 注解就是专门用来解决缓存清除问题的,它可以把一条或多条缓存数据从缓存容器里移除。本文结合实际项目经验,详细讲解 @CacheEvict 的各个属性、典型用法以及容易踩的坑。

Spring Boot 中 @CacheEvict 注解如何正确清除缓存?常见用法与避坑指南

一、@CacheEvict 的核心属性详解

@CacheEvict 注解的定义并不复杂,但它的几个属性直接决定了缓存清除的行为。先看一个最基础的使用示例:

@Service
public class UserService {

    // 查询时写入缓存
    @Cacheable(value = "user", key = "#id")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }

    // 更新时清除对应 key 的缓存
    @CacheEvict(value = "user", key = "#id")
    public void updateUser(Long id, User user) {
        userMapper.updateById(id, user);
    }
}

上面这段代码中,@Cacheable 和 @CacheEvict 使用了相同的缓存名称 user 和相同的 key 表达式 #id,这保证了写缓存和清缓存操作指向同一条缓存记录。key 属性支持 SpEL 表达式,可以引用方法参数、调用结果等,例如 #user.id 表示取参数 user 的 id 字段作为缓存的键。

真正需要重点理解的是 allEntries 属性。当缓存清除操作无法精确定位到某一条缓存时,比如批量删除用户的方法,可以把 allEntries 设置为 true,表示清空整个缓存名称下的所有条目:

@CacheEvict(value = "user", allEntries = true)
public void deleteAllUsers() {
    userMapper.deleteAll();
}

需要注意的是,allEntries 为 true 时不能再指定 key 属性,否则启动阶段或运行阶段会抛出异常。另外一个细节是,开启 allEntries 后 Spring 会遍历删除该缓存下的所有 key,如果使用 Redis 作为缓存实现,某些版本底层会执行 keys 命令扫描,在大 key 量的场景下可能造成 Redis 阻塞,这一点在后面章节还会展开讨论。

beforeInvocation 属性也是一个容易被忽视的点。默认值为 false,表示在方法成功执行完毕后才清除缓存;如果方法执行过程中抛出异常,缓存不会被清除。把它设置为 true 则表示方法执行前就先清缓存,无论方法成功与否。两种方式各有适用场景:对于更新操作,通常保持默认即可,因为方法失败时数据没变,缓存也就没必要清;但如果方法内部逻辑复杂、抛异常概率高,且旧缓存确实已经不可信,那么 beforeInvocation = true 会更安全。

二、@CacheEvict 与 @CachePut 的区别和选择

很多初学者会把 @CacheEvict 和 @CachePut 搞混,两者都能配合数据更新操作使用,但语义完全不同。@CachePut 的作用是执行方法并把返回值放进缓存,相当于更新缓存;而 @CacheEvict 是删除缓存,下次查询时再重新加载。两种策略的对比可以用下面的表格来梳理:

对比项@CachePut@CacheEvict
缓存操作用方法返回值更新缓存删除指定缓存条目
查询缓存不查询,每次都执行方法不查询
典型场景更新后立即需要最新数据更新后允许缓存短暂未命中
风险点返回值与缓存 key 不匹配时缓存污染清除不彻底导致脏数据

从工程实践来看,删缓存加懒加载的组合更稳妥。因为更新缓存的方案要求方法返回值能够完整覆盖缓存中的数据结构,一旦返回对象不完整,就会把残缺数据写进缓存。而删除缓存的方案即使出现短暂的缓存未命中,也只是多查一次数据库,不会产生错误数据。这也是业界常说的 Cache Aside 模式中推荐先更新数据库再删缓存的原因。

还要提醒一点,@CacheEvict 和 @CachePut 都是每次都会执行方法的注解,它们不会像 @Cacheable 那样命中缓存就跳过方法执行。如果把 @CacheEvict 放在一个纯查询方法上,既没有意义,还会让每次查询都触发一次缓存删除,纯属浪费。

三、整合 Redis 作为缓存实现时的完整示例

Spring Boot 默认使用内存中的 ConcurrentHashMap 作为缓存实现,只适合单机测试。生产环境一般会整合 Redis。首先引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

然后在配置类上开启缓存管理并做一些定制化设置,例如给缓存设置过期时间:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeValuesWith(
                        SerializationPair.fromSerializer(
                                new GenericJackson2JsonRedisSerializer()))
                // key 格式:缓存名::参数值
                .computePrefixWith(name -> name + "::");

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .withCacheConfiguration("user",
                        config.entryTtl(Duration.ofHours(2)))
                .build();
    }
}

这个配置里给 user 缓存单独设置了两个小时的过期时间,其他缓存默认三十分钟。给缓存设置 TTL 是配合 @CacheEvict 的兜底手段:即使某次清除操作因为 Bug 被漏掉了,缓存到期后也会自动失效,脏数据的存活时间就有了上限。

使用 Redis 时还有一个与 allEntries 相关的性能问题需要留意。RedisCacheManager 在清空整个缓存时,默认采用逐个遍历 key 删除的方式。如果某个缓存名称下的 key 数量达到几十万级别,这个操作可能会持续数秒,期间还会占用 Redis 的 CPU。对此的改进方案是利用 Redis 的缓存空值或者按业务维度拆分缓存名称,让每个缓存下的 key 数量保持可控;也可以通过自定义 RedisCacheWriter 使用 SCAN 命令替代全量遍历。

四、常见踩坑点与排查思路

第一个坑是 key 表达式不一致导致清不掉缓存。写缓存的 key 是 #id,清缓存时写成了 #user.id,但方法参数里根本没有 user,结果表达式解析失败或者解析出错误的 key。建议在团队规范中约定:同一个实体的缓存 key 表达式统一定义,必要时通过自定义 KeyGenerator 来固化 key 生成逻辑,避免每个人手写表达式带来偏差。

第二个坑是同类内部方法调用导致注解失效。Spring Cache 基于动态代理实现,如果在同一个类里,方法 A 直接调用带 @CacheEvict 的方法 B,这次调用不会走代理,注解完全不生效。解决办法要么把方法拆到另一个 Bean 中,要么注入自身代理对象再调用。这是 Spring AOP 的通用限制,不只是缓存注解会遇到。

第三个坑是缓存清理的顺序问题。推荐的做法是先执行数据库更新,再执行缓存删除。如果反过来,在高并发下可能出现这种情况:请求一删除缓存后,请求二查库拿到旧数据(事务还没提交),把旧数据写回缓存,随后请求一提交事务,缓存里就永久留下了旧数据。虽然概率低,但排查起来非常困难。除了调整顺序,还可以通过延迟双删、设置较短 TTL、或者引入消息队列异步删除等方式进一步降低风险。

最后一点,排查缓存清除问题时可以打开 Spring Cache 的日志,或者直接用 redis-cli 的 MONITOR 命令观察 DEL 命令是否发出、目标 key 是否正确。很多时候问题不在框架,而在 key 拼接细节上,肉眼盯一下实际的 key 字符串往往比看代码更快定位问题。

总结

@CacheEvict 看似只是一个简单的注解,实际落地时需要考虑 key 设计、allEntries 的性能影响、beforeInvocation 的异常语义,以及和数据库操作顺序的配合。核心原则可以归纳为一句话:写缓存和清缓存必须指向同一个 key,更新数据库成功后再删缓存,同时用 TTL 做兜底。把这几个细节处理好,缓存方案就能在性能和数据一致性之间取得比较好的平衡。

Spring BootCacheEvictSpring Cache修改时间:2026-09-09 08:12:43

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260909/53283.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。