2. Spring Boot 缓存抽象:@Cacheable、@CachePut、@CacheEvict 注解详解与配置
Spring 框架从 3.1 版本开始引入了声明式缓存抽象,它通过一组注解将缓存操作与业务逻辑解耦。开发者无需手动编写缓存读写代码,只需在方法上添加注解,即可实现缓存数据的存取、更新与失效。本章将深入剖析三个核心注解 @Cacheable、@CachePut 和 @CacheEvict 的工作原理、配置细节及最佳实践。
2.1 缓存抽象核心概念
在深入注解之前,需要理解 Spring 缓存抽象中的几个关键组件:
| 组件 | 说明 | 类比 |
|---|---|---|
| CacheManager | 缓存管理器,管理多个 Cache 实例,负责缓存的创建、获取和生命周期管理 |
数据库连接池 |
| Cache | 缓存容器,存储键值对,提供 get、put、evict 等原子操作 |
数据库中的一张表 |
| Cache Key | 缓存键,默认基于方法参数生成,可通过 key 属性自定义 |
数据库主键 |
| Cache Value | 缓存值,即方法的返回结果 | 数据库行记录 |
CacheManager 接口统一管理。切换缓存中间件只需更换配置,业务代码无需改动。
2.2 @Cacheable:缓存读取与存储
@Cacheable 是最常用的缓存注解,用于标记需要缓存结果的方法。当方法被调用时,Spring 会先检查缓存中是否存在对应键的值:
- 缓存命中: 直接返回缓存值,不执行方法体。
- 缓存未命中: 执行方法体,将返回值存入缓存,再返回结果。
2.2.1 基本用法与属性
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// 模拟耗时数据库查询
simulateSlowQuery();
return userRepository.findById(id).orElse(null);
}
}
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value / cacheNames |
String[] | 无(必填) | 缓存名称,对应 CacheManager 中的缓存区域 |
key |
String | SpEL 表达式,默认基于所有参数 | 自定义缓存键,支持 Spring Expression Language (SpEL) |
condition |
String | ""(无条件) | SpEL 条件表达式,为 true 时才缓存 |
unless |
String | ""(无条件) | SpEL 条件表达式,为 true 时不缓存 |
sync |
boolean | false | 是否启用同步模式,防止缓存击穿 |
2.2.2 缓存键生成策略
Spring 使用 KeyGenerator 接口生成缓存键。默认实现 SimpleKeyGenerator 的规则如下:
- 无参数: 使用
SimpleKey.EMPTY(空键)。 - 一个参数: 直接使用该参数作为键。
- 多个参数: 使用
SimpleKey对象包装所有参数。
key 属性。例如 key = "#userId",否则默认使用所有参数组合,可能导致缓存键不符合预期。
2.2.3 condition 与 unless 的区别
| 属性 | 评估时机 | 作用 | 示例 |
|---|---|---|---|
condition |
方法执行前 | 满足条件才检查并缓存 | condition = "#id > 0" |
unless |
方法执行后 | 满足条件则不缓存结果 | unless = "#result == null" |
@Cacheable(value = "users", key = "#id",
condition = "#id != null",
unless = "#result == null")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
2.2.4 sync 同步模式
在高并发场景下,多个线程同时请求同一个未缓存的 key 会导致缓存击穿。设置 sync = true 后,Spring 会对缓存键加锁,确保只有一个线程执行方法体,其他线程等待结果:
@Cacheable(value = "users", key = "#id", sync = true)
public User getUserById(Long id) {
// 高并发下只有一个线程执行此方法
return userRepository.findById(id).orElse(null);
}
sync 模式使用局部锁,仅对同一个 JVM 实例有效。在分布式环境下,仍需结合分布式锁或缓存中间件自身的原子操作来防止击穿。
2.3 @CachePut:缓存更新
@CachePut 注解用于强制更新缓存。与 @Cacheable 不同,它始终执行方法体,并将返回值更新到缓存中。常用于数据更新操作后刷新缓存。
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
| 对比维度 | @Cacheable | @CachePut |
|---|---|---|
| 方法是否执行 | 缓存命中时不执行 | 始终执行 |
| 缓存写入时机 | 仅未命中时写入 | 每次调用都写入 |
| 典型场景 | 查询操作 | 更新/插入操作 |
| 与数据库一致性 | 可能读到旧数据 | 保证缓存与数据库同步 |
@CachePut 和 @CacheEvict 的组合,可以灵活控制缓存更新策略。例如:先更新缓存(@CachePut),再清理关联查询的缓存(@CacheEvict)。
2.4 @CacheEvict:缓存失效
@CacheEvict 用于从缓存中移除数据,通常在删除或更新操作后调用,避免脏数据残留。
2.4.1 基本用法
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
2.4.2 关键属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
allEntries |
boolean | false | 是否清空整个缓存区域 |
beforeInvocation |
boolean | false | 是否在方法执行前清除缓存 |
2.4.3 allEntries:批量清空
当需要清空某个缓存区域的所有数据时,设置 allEntries = true:
@CacheEvict(value = "users", allEntries = true)
public void clearAllUserCache() {
// 方法体可以为空,仅用于触发缓存清理
}
allEntries = true 会清空整个缓存区域,影响所有缓存键。在生产环境中应谨慎使用,避免大量缓存同时失效导致数据库压力骤增(缓存雪崩)。
2.4.4 beforeInvocation:执行时机
默认情况下,@CacheEvict 在方法执行成功返回后才清除缓存。如果方法抛出异常,缓存不会被清除。设置 beforeInvocation = true 可以在方法执行前清除缓存:
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUserWithException(Long id) {
// 即使此方法抛出异常,缓存也会被清除
userRepository.deleteById(id);
throw new RuntimeException("模拟异常");
}
2.5 注解组合与缓存一致性闭环
在实际业务中,单一注解往往无法满足复杂的缓存一致性需求。下图展示了典型的缓存一致性闭环流程:
2.6 缓存配置与启用
2.6.1 启用缓存抽象
在 Spring Boot 中,只需在配置类或启动类上添加 @EnableCaching 注解即可启用缓存抽象:
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.6.2 配置缓存管理器
Spring Boot 自动配置了 CacheManager,但可以通过配置文件自定义缓存行为。以 Caffeine 缓存为例:
# application.yml
spring:
cache:
type: caffeine
cache-names: users,products,orders
caffeine:
spec: maximumSize=500,expireAfterWrite=10m
| 配置项 | 说明 | 示例值 |
|---|---|---|
spring.cache.type |
缓存类型(caffeine, redis, simple 等) | caffeine |
spring.cache.cache-names |
预定义的缓存区域名称列表 | users, products |
spring.cache.caffeine.spec |
Caffeine 缓存规范 | maximumSize=500,expireAfterWrite=10m |
2.6.3 自定义 KeyGenerator
当默认的键生成策略不满足需求时,可以自定义 KeyGenerator:
@Configuration
public class CacheConfig {
@Bean("customKeyGenerator")
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName());
sb.append(".");
sb.append(method.getName());
sb.append("(");
for (Object param : params) {
sb.append(param).append(",");
}
sb.append(")");
return sb.toString();
};
}
}
// 使用自定义 KeyGenerator
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
2.7 常见问题与最佳实践
原因:Spring 缓存基于 AOP 代理实现,同一个类中的方法调用(this.method())不会经过代理,导致缓存注解不生效。
解决方案:将缓存方法抽取到另一个 Service Bean 中,或通过
AopContext.currentProxy() 获取代理对象。
当查询数据库返回 null 时,默认不会缓存。这会导致每次请求都穿透到数据库。解决方案:使用
unless = "#result == null" 配合 Optional 或默认值,或者缓存一个特殊标记值。
- 始终为缓存键指定有意义的 SpEL 表达式,避免默认键冲突。
- 更新操作使用
@CachePut保证缓存与数据库同步。 - 删除操作使用
@CacheEvict避免脏数据。 - 高并发查询场景开启
sync = true防止缓存击穿。 - 合理设置缓存过期时间,避免数据长期不一致。
- 使用
allEntries = true时注意缓存雪崩风险。