2. Spring Boot缓存抽象:Spring Cache抽象层介绍、@Cacheable/@CachePut/@CacheEvict注解详解、缓存管理器(CacheManager)配置
2.1 Spring Cache抽象层设计理念
Spring Framework从3.1版本开始引入缓存抽象层(Spring Cache Abstraction),其核心设计思想是:将缓存操作与业务逻辑解耦,通过声明式注解实现缓存读写,开发者无需直接操作底层缓存客户端(如Redis、Ehcache、Caffeine等)。
抽象层位于应用程序与具体缓存实现之间,提供统一的编程模型。其架构层次如下:
CacheManager和Cache是核心接口,所有缓存实现都实现这两个接口。业务代码通过注解声明缓存行为,运行时由AOP切面拦截方法调用,自动执行缓存读写。
2.2 核心注解详解
2.2.1 @Cacheable — 缓存读取注解
@Cacheable用于标记方法,表示方法的返回值可以被缓存。当方法被调用时,Spring先检查缓存中是否存在对应key的数据,如果存在则直接返回缓存值,不执行方法体;如果不存在则执行方法,并将返回值存入缓存。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value / cacheNames | String[] | — | 缓存名称,指定缓存区域,可指定多个 |
key | String | 方法参数组合 | SpEL表达式,自定义缓存key |
keyGenerator | String | — | 自定义key生成器的Bean名称 |
cacheManager | String | — | 指定使用的缓存管理器Bean名称 |
condition | String | — | SpEL条件表达式,满足条件才缓存 |
unless | String | — | SpEL条件表达式,满足条件时不缓存 |
sync | boolean | false | 是否同步模式,防止缓存击穿 |
// 基础用法:缓存用户查询结果
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// 模拟数据库查询
return userRepository.findById(id).orElse(null);
}
// 多条件组合key
@Cacheable(value = "users", key = "#name + ':' + #age")
public List<User> findUsersByNameAndAge(String name, Integer age) {
return userRepository.findByNameAndAge(name, age);
}
// 条件缓存:仅当userId大于1000时才缓存
@Cacheable(value = "users", condition = "#id > 1000")
public User getSpecialUser(Long id) {
return userRepository.findById(id).orElse(null);
}
// 排除缓存:返回值为null时不缓存
@Cacheable(value = "users", unless = "#result == null")
public User getUserOrNull(Long id) {
return userRepository.findById(id).orElse(null);
}
// 同步模式:防止缓存击穿(高并发场景推荐)
@Cacheable(value = "users", key = "#id", sync = true)
public User getUserSync(Long id) {
return userRepository.findById(id).orElse(null);
}
sync = true,Spring会通过锁机制保证同一时刻只有一个线程执行方法体,其他线程等待缓存结果,有效防止缓存击穿。
2.2.2 @CachePut — 缓存更新注解
@CachePut始终执行方法体,并将返回值更新到缓存中。与@Cacheable不同,它不会检查缓存是否存在,每次都会执行方法。常用于数据更新操作后刷新缓存。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value / cacheNames | String[] | — | 缓存名称,指定缓存区域 |
key | String | 方法参数组合 | SpEL表达式,自定义缓存key |
condition | String | — | 满足条件才更新缓存 |
unless | String | — | 满足条件时不更新缓存 |
// 更新用户信息后刷新缓存
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
// 条件更新:仅当返回值不为null时更新缓存
@CachePut(value = "users", key = "#id", unless = "#result == null")
public User updateUserName(Long id, String newName) {
User user = userRepository.findById(id).orElse(null);
if (user != null) {
user.setName(newName);
return userRepository.save(user);
}
return null;
}
2.2.3 @CacheEvict — 缓存驱逐注解
@CacheEvict用于删除缓存数据,通常在数据删除或批量更新时使用。可以删除单个缓存条目,也可以清空整个缓存区域。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value / cacheNames | String[] | — | 缓存名称,指定缓存区域 |
key | String | — | SpEL表达式,指定要删除的缓存key |
allEntries | boolean | false | 是否清空整个缓存区域 |
beforeInvocation | boolean | false | 是否在方法执行前清除缓存 |
condition | String | — | 满足条件才清除缓存 |
// 删除单个缓存条目
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// 清空整个users缓存区域
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsersCache() {
// 方法体可以为空,仅用于触发缓存清空
}
// 方法执行前清除缓存(防止方法执行失败导致缓存未清除)
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUserWithPreEvict(Long id) {
userRepository.deleteById(id);
}
// 组合注解:更新时先驱逐旧缓存再写入新缓存
@Caching(evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "userLists", allEntries = true)
})
public User updateUserWithEvict(User user) {
return userRepository.save(user);
}
@CacheEvict默认在方法执行成功后清除缓存。如果方法抛出异常,缓存不会被清除。设置beforeInvocation = true可以在方法执行前清除缓存,但需注意如果方法执行失败,缓存已被清除,下次查询会重新加载数据。
2.2.4 @Caching — 组合注解
当需要同时应用多个缓存注解时,使用@Caching进行组合。例如:更新数据时需要同时更新多个缓存区域,或者同时执行缓存更新和缓存驱逐。
@Caching(
put = {
@CachePut(value = "users", key = "#user.id"),
@CachePut(value = "userNames", key = "#user.name")
},
evict = {
@CacheEvict(value = "userLists", allEntries = true)
}
)
public User saveUser(User user) {
return userRepository.save(user);
}
2.3 缓存管理器(CacheManager)配置
CacheManager是Spring Cache抽象层的核心管理器接口,负责创建和管理Cache实例。Spring Boot通过自动配置为常见缓存实现提供默认的CacheManager,也支持手动配置实现精细控制。
2.3.1 常用CacheManager实现对比
| 实现类 | 底层存储 | 适用场景 | 特点 |
|---|---|---|---|
ConcurrentMapCacheManager | ConcurrentHashMap | 单机开发/测试 | 简单、无序列化、数据不持久 |
CaffeineCacheManager | Caffeine(内存) | 单机高性能 | 支持过期、最大容量、统计 |
RedisCacheManager | Redis | 分布式缓存 | 支持持久化、集群、TTL |
EhCacheCacheManager | Ehcache(磁盘+内存) | Java EE传统项目 | 支持磁盘持久化、XML配置 |
JCacheCacheManager | JCache (JSR-107) | 标准兼容 | 遵循JCache规范 |
2.3.2 配置RedisCacheManager(最常用方案)
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默认缓存配置:全局TTL 10分钟
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// 为不同缓存区域设置不同TTL
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put("users", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues());
cacheConfigurations.put("sessions", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(2))
.disableCachingNullValues());
cacheConfigurations.put("configs", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.disableCachingNullValues());
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.transactionAware() // 支持事务
.build();
}
}
2.3.3 配置CaffeineCacheManager(单机高性能方案)
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 默认配置:最大容量1000,写入后10分钟过期
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()); // 开启统计
// 支持按缓存名称自定义配置
cacheManager.setCacheNames(Arrays.asList("users", "sessions", "configs"));
return cacheManager;
}
// 更精细的配置:为不同缓存区域设置不同策略
@Bean
public CacheManager customCaffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<Cache> caches = new ArrayList<>();
caches.add(new CaffeineCache("users", Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build()));
caches.add(new CaffeineCache("sessions", Caffeine.newBuilder()
.maximumSize(2000)
.expireAfterWrite(2, TimeUnit.HOURS)
.build()));
cacheManager.setCaches(caches);
return cacheManager;
}
}
2.3.4 多缓存管理器共存配置
实际项目中可能需要同时使用多种缓存实现,例如:热点数据使用Caffeine本地缓存,全量数据使用Redis分布式缓存。Spring Cache支持配置多个CacheManager,通过@Cacheable的cacheManager属性指定。
@Configuration
@EnableCaching
public class MultiCacheManagerConfig {
@Primary
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("hotData");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES));
return cacheManager;
}
}
// 使用示例:热点数据使用Caffeine本地缓存
@Cacheable(value = "hotData", key = "#id", cacheManager = "caffeineCacheManager")
public HotData getHotData(Long id) {
return hotDataRepository.findById(id).orElse(null);
}
// 全量数据使用Redis分布式缓存
@Cacheable(value = "users", key = "#id", cacheManager = "redisCacheManager")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
@Primary标记主缓存管理器。业务代码中通过cacheManager属性显式指定,避免歧义。实际生产环境中,可结合Caffeine作为一级缓存、Redis作为二级缓存,实现“本地缓存+分布式缓存”的多级缓存架构,兼顾性能与一致性。
2.4 缓存注解执行流程
Spring Cache通过AOP切面实现缓存逻辑,其核心执行流程如下:
- @Cacheable:先查缓存 → 命中则直接返回 → 未命中则执行方法 → 将结果存入缓存 → 返回结果
- @CachePut:直接执行方法 → 将结果存入缓存 → 返回结果(每次都会更新缓存)
- @CacheEvict:(默认)先执行方法 → 清除缓存 → 返回结果;(beforeInvocation=true)先清除缓存 → 执行方法 → 返回结果
2.5 缓存Key生成策略
Spring Cache默认使用SimpleKeyGenerator生成缓存key,规则如下:
- 无参数方法:使用
SimpleKey.EMPTY - 单个参数方法:直接使用该参数作为key
- 多个参数方法:使用
SimpleKey对象包装所有参数
自定义key生成器:
// 自定义KeyGenerator
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 格式:类名.方法名:参数1_参数2
return target.getClass().getSimpleName() + "." + method.getName()
+ ":" + Arrays.stream(params).map(String::valueOf).collect(Collectors.joining("_"));
}
}
// 使用自定义KeyGenerator
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User findUser(Long id, String name) {
// 生成的key示例:UserService.findUser:100_张三
return userRepository.findByIdAndName(id, name);
}
| 表达式 | 说明 |
|---|---|
#id | 方法参数id |
#user.id | 参数对象的属性 |
#result | 方法返回值(仅用于unless) |
#root.methodName | 当前方法名 |
#root.targetClass | 目标类 |
#root.args[0] | 第一个参数 |
#name + ':' + #age | 字符串拼接 |
2.6 常见问题与避坑指南
this.getUser())不会经过代理,缓存注解失效。
解决方案:
- 将缓存方法抽取到独立的Service类中
- 注入自身代理:
@Autowired private UserService self;然后调用self.getUser() - 使用
AopContext.currentProxy()获取当前代理对象
- 所有缓存注解方法必须为
public,private/protected方法不会触发缓存 - 缓存key尽量使用业务唯一标识,避免使用对象引用
- 合理设置TTL,避免缓存雪崩(建议加随机偏移量)
- 写操作(增删改)务必同步清除或更新相关缓存
- 使用
@Cacheable(sync=true)应对高并发缓存击穿 - 生产环境禁用
ConcurrentMapCacheManager,使用Redis或Caffeine