Spring Boot 的方法级缓存抽象并不是直接操作 Redis 或 Caffeine,而是在服务方法外面包了一层代理,通过 CacheManager 统一管理缓存操作。@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