导读:本期聚焦于辉辉创作的《如何在 Spring Boot 中使用 @Cacheable 注解实现方法级缓存抽象?》,敬请观看详情。接口响应变慢时,除了优化 SQL 和添加索引,缓存往往是最直接的提速手段。Spring Boot 提供的方法级缓存抽象允许开发者在服务层方法上声明缓存行为,其中 @Cacheable 注解负责读取缓存,命中时直接返回缓存结果,跳过方法体执行。它的工作链路包括缓存键生成、CacheManager 管理、条件判断和失效策略。配置好 Redis 或本地缓存后,只需在方法上标注注解即可完成缓存接入,但参数组合、返回值类型、缓存穿透和缓存一致性等细节会影响最终效果。本文将拆解 @Cacheable 的核心参数、执行流程和常见坑点,并结合代码演示如何让缓存真正生效。

Spring Boot 的方法级缓存抽象并不是直接操作 Redis 或 Caffeine,而是在服务方法外面包了一层代理,通过 CacheManager 统一管理缓存操作。@Cacheable 作为这一层最常用的注解,可以在方法执行前先查缓存,命中就直接返回缓存值,未命中再执行方法体并把返回值写入缓存。理解这个代理链路,能够帮助开发者避免很多缓存不生效或缓存错乱的问题。

如何在 Spring Boot 中使用 @Cacheable 注解实现方法级缓存抽象?

@Cacheable 的核心机制与参数解析

Spring Cache 的核心接口是 Cache 和 CacheManager。CacheManager 负责管理一组 Cache,每一个 Cache 都可以看作一个命名空间。方法上的 @Cacheable 注解通过 value 或 cacheNames 属性指定使用哪个缓存空间,底层 CacheManager 会根据名字找到对应的 Cache 实例。如果缓存中没有对应 key 的值,目标方法才会执行,返回值随后被写入缓存;如果缓存命中,则方法体根本不会进入。

@Cacheable 还有几个重要属性。key 属性用来控制缓存键的生成逻辑,默认情况下 Spring 使用 SimpleKeyGenerator,它会把所有参数组合成一个 SimpleKey。cacheManager 和 cacheResolver 可以用来指定使用哪个缓存管理器,适合多缓存源并存的项目。condition 和 unless 都支持 SpEL 表达式,前者决定方法执行前是否查缓存,后者决定返回值是否写入缓存。sync 属性只对本地缓存或支持同步的缓存实现有效,可以避免同一 key 在高并发下同时回源。

@Service
public class GoodsService {

    @Cacheable(value = "goods", key = "#id")
    public Goods getGoodsById(Long id) {
        return goodsMapper.selectById(id);
    }
}

上面代码中,@Cacheable 指定缓存空间为 goods,并显式把参数 id 作为缓存键。这样当传入的 id 相同,方法不会重复执行数据库查询,而是直接返回缓存对象。需要注意的是,如果方法返回值类型不是可序列化的,本地缓存一般不强求,但接入 Redis 时必须保证对象可以被序列化,否则写入缓存时可能报错。

SpEL 表达式在 key、condition、unless 中的实战

SpEL 是 Spring 表达式语言,@Cacheable 的 key、condition、unless 都可以使用它。SpEL 上下文里可以访问方法参数名,也可以通过 #root 对象访问目标方法、目标类和参数数组。常用写法包括:用 #id 作为简单键,用 #user.id 取对象属性,用 #root.args[0] 取第一个参数。写条件时可以组合逻辑表达式,例如 condition = "#id > 0" 表示只有 id 大于 0 时才走缓存。

unless 和 condition 容易混淆。condition 在方法执行前判断,如果表达式为 false,会跳过缓存读取,直接执行方法;unless 在方法执行后判断,如果表达式为 true,则不写入缓存。比如 unless = "#result == null",意思是返回值为 null 不缓存。这个配置对防止缓存穿透很有用,但要注意如果返回 null 本身有业务含义,可能还需要配合短 TTL。

@Cacheable(
    value = "user",
    key = "#userId + ':' + #source",
    condition = "#userId > 0",
    unless = "#result == null"
)
public User getUser(Long userId, String source) {
    return userMapper.selectUser(userId, source);
}

如果需要使用参数名作为 SpEL 变量,编译时需要保留参数名。Maven 和 Gradle 项目通常可以通过编译参数 -parameters 实现,Spring Boot 2 之后大部分场景下直接写参数名就能生效。如果偶发出现 SpEL 解析失败,可改用 #root.args 这种不受参数名影响的写法。例如 key = "#root.args[0] + ':' + #root.args[1]"。

与 Redis 集成并处理序列化、过期时间和缓存穿透

Spring Boot 默认的缓存实现是基于 ConcurrentHashMap 的本地缓存,适合单机或测试环境。真正的生产环境通常会接入 Redis,借助集中式缓存实现多实例共享。引入 spring-boot-starter-data-redis 后,需要手动定义一个 RedisCacheManager,并通过 RedisCacheConfiguration 设置 key 和 value 的序列化器、缓存过期时间等。

Redis 默认使用 JdkSerializationRedisSerializer,它要求对象实现 Serializable,可读性也差。更推荐使用 StringRedisSerializer 作为 key 序列化器,使用 GenericJackson2JsonRedisSerializer 作为 value 序列化器。这样 key 在 Redis 里是明确字符串,value 是 JSON 文本,排查问题更方便。如果使用 LocalDateTime 等日期类型,还要给 Jackson 配置 JavaTimeModule,否则序列化会失败。

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new StringRedisSerializer())
            )
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer())
            )
            .disableCachingNullValues();

        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .transactionAware()
            .build();
    }
}

上面配置设置了 30 分钟过期时间,并禁用了 null 值缓存。如果业务上确实需要缓存 null 来防止缓存穿透,可以去掉 disableCachingNullValues,同时给 null 值配置较短 TTL。要注意 RedisCacheManager 的配置是全局默认值,不同缓存空间如果需要不同过期时间,可以通过 withCacheConfiguration 单独指定,而不是在一个方法上硬编码 TimeUnit。

缓存更新与失效:@CachePut、@CacheEvict、@Caching 组合

缓存只读场景并不多,更多时候数据会更新。@CachePut 会在方法执行后把返回值放入缓存,不会跳过方法体,因此适合更新场景。@CacheEvict 负责删除缓存,可以指定 key,也可以设置 allEntries = true 清空整个缓存空间。beforeInvocation 属性决定在方法执行前还是执行后删除缓存,默认是 false,即方法成功后才删除。

典型做法是:查询方法使用 @Cacheable,新增和修改方法使用 @CachePut 或 @CacheEvict,删除方法使用 @CacheEvict。需要注意 @CachePut 和 @Cacheable 的 key 表达式必须完全一致,否则更新后写入的键和查询读取的键不一致,缓存不会生效。@Caching 用来组合多个缓存操作,适合一个方法需要同时删除多个缓存空间的场景。

@Cacheable(value = "goods", key = "#id")
public Goods getGoods(Long id) {
    return goodsMapper.selectById(id);
}

@CachePut(value = "goods", key = "#goods.id")
public Goods updateGoods(Goods goods) {
    goodsMapper.updateById(goods);
    return goods;
}

@CacheEvict(value = "goods", key = "#id")
public void deleteGoods(Long id) {
    goodsMapper.deleteById(id);
}

数据库和缓存的一致性问题并不能只靠注解完全解决。常见策略是先更新数据库,再删除缓存,而不是先更新缓存。因为删除缓存后,下一次读请求会自然回源并重建缓存,既能避免双写顺序问题,也能降低并发窗口。若业务必须强一致,可以引入事务消息或延迟双删,但这会明显增加系统复杂度。

常见的缓存失效场景与排查思路

很多项目里加了 @Cacheable 却发现缓存不生效,最常见的原因是类内部方法调用。Spring 的缓存注解依赖 AOP 代理,只有通过 Spring 容器获取的 Bean 调用方法时,代理才会拦截并处理缓存。如果同一个类里用 this 调用带 @Cacheable 的方法,实际调用的是原始对象方法,注解自然不生效。解决办法是拆分到不同 Bean,或者通过 ApplicationContext 获取代理对象。

另一个常见问题是缓存 key 冲突。两个方法使用同一个缓存空间,但 key 表达式不同,可能导致碰撞。假设一个方法 key 写 #id,另一个方法 key 写 #userId,在参数值相同但对象完全不同的情况下,缓存返回错误类型的数据。更好的习惯是在缓存空间和 key 中加入业务前缀,例如 key = "'goods:' + #id"。序列化问题也很典型,切换 Redis 序列化器后旧数据不一定兼容,必要时可以清空对应缓存空间。

排查缓存是否命中时,可以先在 Redis 客户端查看 key 是否存在,也可以临时打开 Spring 的 debug 日志观察 CacheInterceptor 行为。还可以在方法体内打印日志,如果日志一直重复出现,说明缓存没有命中;如果日志只出现一次,说明缓存生效。生产环境建议增加缓存命中率监控,以便及时发现 key 设计不合理或 TTL 设置过短的问题。

Spring Boot@Cacheable方法级缓存修改时间:2026-08-26 21:21:39

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