跳转到内容

缓存注解实战


Spring Cache 是 Spring 提供的缓存抽象层,通过注解声明式地为方法添加缓存,业务代码无需关心缓存的读写细节。底层缓存实现可以是 Redis、Caffeine、Ehcache 等,只需更换配置即可切换,业务代码不变。

方法调用流程(@Cacheable):
第一次调用:
请求 ──▶ 查缓存(未命中)──▶ 执行方法 ──▶ 结果写入缓存 ──▶ 返回结果
后续调用:
请求 ──▶ 查缓存(命中)──▶ 直接返回缓存值(方法不执行)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

2.2. 开启缓存 + 配置 Redis 序列化

Section titled “2.2. 开启缓存 + 配置 Redis 序列化”
@Configuration
@EnableCaching // 开启 Spring Cache 注解支持
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // 默认缓存过期时间
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)))
.disableCachingNullValues(); // 不缓存 null 值
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}

方法执行前先查缓存,命中则直接返回,不执行方法;未命中则执行方法并将结果存入缓存。

@Service
public class UserService {
// 最基础用法:缓存 key = "user::1001"
@Cacheable(cacheNames = "user", key = "#id")
public User getUserById(Long id) {
return userMapper.selectById(id); // 缓存命中时此行不执行
}
// 使用 SpEL 构造复合 key
@Cacheable(cacheNames = "user", key = "#p0 + ':' + #p1")
public List<User> getUserByPage(int page, int size) {
return userMapper.selectPage(page, size);
}
// 条件缓存:id > 0 才缓存
@Cacheable(cacheNames = "user", key = "#id", condition = "#id > 0")
public User getUser(Long id) {
return userMapper.selectById(id);
}
// unless:方法执行后判断,结果为 null 时不缓存
@Cacheable(cacheNames = "user", key = "#id", unless = "#result == null")
public User getUserOrNull(Long id) {
return userMapper.selectById(id);
}
}

SpEL 表达式速查:

表达式说明示例
#参数名方法参数值#id
#p0 / #p1按位置取参数#p0
#result方法返回值(unless 中用)#result.id
#root.method当前方法
'字面量'字符串常量'user'

方法一定执行,执行后将结果写入缓存(更新或新建)。适合 update 场景,保持缓存与数据库同步。

// 更新用户:DB 更新后同步刷新缓存
@CachePut(cacheNames = "user", key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user; // 返回值会被存入缓存
}

方法执行时删除指定缓存,适合 delete 或复杂 update 场景(直接删除缓存,下次查询时重建)。

// 删除用户:同时清除缓存
@CacheEvict(cacheNames = "user", key = "#id")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
// 清空某个 cacheNames 下的所有缓存
@CacheEvict(cacheNames = "user", allEntries = true)
public void clearAllUserCache() {
// 常用于数据批量变更后刷新全量缓存
}
// beforeInvocation = true:方法执行前删缓存
// 默认 false:方法执行后删缓存(方法抛异常则不删)
@CacheEvict(cacheNames = "user", key = "#id", beforeInvocation = true)
public void deleteUserSafe(Long id) {
userMapper.deleteById(id);
}

一个方法需要同时操作多个缓存时使用:

@Caching(
put = {
@CachePut(cacheNames = "user", key = "#user.id"),
@CachePut(cacheNames = "user:email", key = "#user.email")
},
evict = {
@CacheEvict(cacheNames = "user:list", allEntries = true)
}
)
public User createUser(User user) {
userMapper.insert(user);
return user;
}

3.5. @CacheConfig — 类级别公共配置

Section titled “3.5. @CacheConfig — 类级别公共配置”

抽取公共的 cacheNames,避免每个方法重复写:

@Service
@CacheConfig(cacheNames = "user") // 类级别指定 cacheNames
public class UserService {
@Cacheable(key = "#id") // 无需再写 cacheNames
public User getById(Long id) { ... }
@CacheEvict(key = "#id")
public void delete(Long id) { ... }
}
注解方法是否执行缓存操作典型场景
@Cacheable未命中才执行读缓存,未命中则写入查询接口
@CachePut一定执行写入/更新缓存更新接口
@CacheEvict一定执行删除缓存删除/复杂更新接口
@Caching一定执行组合多个缓存操作复杂业务

默认 key 生成策略不直观,建议自定义:

@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
// 格式:类名:方法名:参数1:参数2
String className = target.getClass().getSimpleName();
String methodName = method.getName();
String paramStr = Arrays.stream(params)
.map(Object::toString)
.collect(Collectors.joining(":"));
return className + ":" + methodName + ":" + paramStr;
};
}
// 使用自定义 KeyGenerator
@Cacheable(cacheNames = "user", keyGenerator = "keyGenerator")
public User getUser(Long id) { ... }
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// 默认配置(1 小时过期)
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(...)
.serializeValuesWith(...);
// 为特定 cacheNames 设置不同 TTL
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("user", defaultConfig.entryTtl(Duration.ofHours(2)));
configMap.put("product", defaultConfig.entryTtl(Duration.ofMinutes(30)));
configMap.put("config", defaultConfig.entryTtl(Duration.ofDays(1)));
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configMap)
.build();
}
@Service
@CacheConfig(cacheNames = "product")
@RequiredArgsConstructor
public class ProductService {
private final ProductMapper productMapper;
// 查询:优先从缓存取
@Cacheable(key = "#id", unless = "#result == null")
public Product getById(Long id) {
return productMapper.selectById(id);
}
// 更新:同步刷新缓存
@CachePut(key = "#product.id")
public Product update(Product product) {
productMapper.updateById(product);
return product;
}
// 删除:清除缓存
@CacheEvict(key = "#id")
public void delete(Long id) {
productMapper.deleteById(id);
}
// 批量更新:清空全部缓存,下次查询重建
@CacheEvict(allEntries = true)
public void batchUpdate(List<Product> products) {
productMapper.batchUpdate(products);
}
}