缓存是提升系统性能最直接的手段之一。Spring Boot 对缓存的支持做得相当轻量,不需要手写 RedisTemplate 的增删查逻辑,只要在方法上声明 @Cacheable,框架就会自动拦截方法调用:先查缓存,命中则直接返回,不命中才真正执行方法体并把结果写入缓存。本文从一个可运行的例子出发,完整讲解 Spring Boot 整合缓存的配置方法、各核心注解的区别,以及接入 Redis 后的进阶用法和常见坑。

一、开启缓存支持与 @Cacheable 基础用法
Spring Boot 整合缓存的第一步是引入 spring-boot-starter-cache 依赖,并在配置类上添加 @EnableCaching 注解。这个注解会激活缓存代理,让容器中带有缓存注解的 Bean 被动态代理包裹。Maven 依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>接着在启动类或任意配置类上加 @EnableCaching:
@SpringBootApplication
@EnableCaching // 开启缓存代理
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}使用时只需标注在查询方法上。value 属性指定缓存名称(可以理解为命名空间),key 属性指定缓存键,支持 SpEL 表达式:
@Service
public class UserService {
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) {
System.out.println("真正执行查询方法, id=" + id);
return userMapper.selectById(id);
}
}第一次调用 getUserById(1L) 时控制台会打印日志并查库,第二次调用相同的 id 则直接返回缓存结果,方法体不会执行,这就是 @Cacheable 最核心的行为:先查缓存,命中就跳过方法执行。如果不指定 key,Spring 会使用 SimpleKeyGenerator,把所有入参拼成一个组合键,只有一个参数时直接用该参数作为键。
需要注意 key 的唯一性问题。如果同一个缓存名称下有多个方法,或者方法的参数本身不能唯一标识一条数据,就必须显式指定 key。SpEL 中常用的写法包括 #id(取参数)、#user.id(取对象属性)、#result.id(取返回值,仅 @CachePut 可用)以及 T(java.util.Objects).hash(#a, #b) 这类静态方法调用。
二、@CachePut、@CacheEvict 与 @Caching 的配合使用
缓存不能只进不出,否则更新数据后读到的永远是旧值。Spring 提供了一组注解各司其职,理解它们的执行时机是关键。
@Cacheable 是双行为注解:先查后写。@CachePut 则不同,它每次都会执行方法体,只是把返回值更新到缓存中,适用于更新操作后刷新缓存。@CacheEvict 用于删除缓存,可以按 key 删除,也可以用 allEntries = true 清空整个缓存名称下的所有条目。三者对比如下:
| 注解 | 是否执行方法 | 对缓存的操作 | 典型场景 |
|---|---|---|---|
| @Cacheable | 命中则不执行 | 查询,未命中则写入 | 查询方法 |
| @CachePut | 总是执行 | 用返回值覆盖缓存 | 更新方法 |
| @CacheEvict | 总是执行 | 删除指定 key 或全部条目 | 删除、批量更新 |
一个典型的增删改查组合写法如下:
@Service
public class UserService {
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) { ... }
@CachePut(value = "userCache", key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
@CacheEvict(value = "userCache", key = "#id")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
// 清空整个 userCache,常用于批量操作后
@CacheEvict(value = "userCache", allEntries = true)
public void reloadAll() { ... }
}当一个方法需要同时应用多种缓存操作时,可以使用 @Caching 组合注解,例如同时清空两个缓存空间:
@Caching(evict = {
@CacheEvict("userCache"),
@CacheEvict(value = "deptCache", allEntries = true)
})
public void importUsers(List<User> users) { ... }三、切换到 Redis 缓存并配置序列化与过期时间
不加任何额外依赖时,Spring Boot 使用 ConcurrentMapCacheManager,数据存在 JVM 内存的 ConcurrentHashMap 中。这种方式无法设置过期时间,不支持分布式共享,应用重启缓存全部丢失,只适合开发调试或数据量极小的场景。生产环境基本都要换成 Redis。
引入 Redis 依赖后,Spring Boot 会自动配置 RedisCacheManager:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>在 application.yml 中配置连接信息:
spring:
redis:
host: 127.0.0.1
port: 6379
password: yourpassword
database: 0默认的序列化器是 JDK 序列化,存进 Redis 的值会变成一串不可读的字节码,而且要求实体类实现 Serializable 接口,反序列化时还容易出现版本不一致的问题。推荐自定义配置为 JSON 序列化,同时为不同缓存设置不同的过期时间:
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 默认过期 30 分钟
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
// 单独为某个缓存空间设置更长的过期时间
configs.put("userCache", config.entryTtl(Duration.ofHours(2)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(configs)
.build();
}
}配置完成后,业务代码不需要任何改动,@Cacheable 注解照常使用,底层实现已经悄悄从本地 Map 换成了 Redis。这种面向注解的声明式风格最大的好处就是业务与缓存实现解耦,将来想换成 Caffeine 或多级缓存,只调整 CacheManager 即可。
四、常见踩坑点与排查思路
坑一:同类内部调用导致缓存失效。缓存是基于 AOP 代理实现的,如果在同一个类中,方法 A 直接调用带 @Cacheable 的方法 B,走的是 this 引用而不是代理对象,注解完全不生效。解决办法是把方法拆到另一个 Bean 中,或者注入自身代理、使用 AopContext.currentProxy()。
坑二:缓存 null 值。查询数据库没有结果时返回 null,默认情况下 Spring 不会缓存 null,导致不存在的数据每次都打到数据库,容易引发缓存穿透。可以在方法上设置 unless = "#result == null" 控制写入条件,或者反过来对 null 做短暂缓存来挡住穿透流量。
坑三:key 冲突。不同方法使用了相同的缓存名和 key 规则,导致数据互相覆盖。建议在 key 中带上方法语义前缀,例如 key = "'detail:' + #id",保证不同方法的数据隔离。
坑四:private 方法加注解无效。同样受代理机制限制,@Cacheable 只能标注在 public 方法上,private、final、static 方法都无法被代理拦截,注解会被静默忽略,不报错但也不生效,排查时容易被忽略。
掌握这几个要点后,Spring Boot 的缓存整合基本就没有盲区了。核心思路是:注解声明缓存语义,CacheManager 决定存储实现,代理机制负责拦截执行。理解了这三层关系,无论是本地缓存、Redis 还是更复杂的多级缓存方案,都可以平滑迁移。
Spring BootCacheable缓存修改时间:2026-09-03 04:44:49