Spring Cache
一、核心概念
Spring Cache 是 Spring Framework 提供的缓存抽象层,它通过注解(如 @Cacheable)屏蔽了底层缓存实现的差异。当与 Redis 结合时,缓存数据存储在 Redis 中,实现分布式共享缓存。
为什么使用 JSON 序列化?
- 默认使用 JDK 序列化,数据为二进制,不可读
- JSON 格式人类可读、跨语言、体积更小(约节省 50% 空间)
- 缓存类无需实现
Serializable接口
二、引入依赖
在 pom.xml 中添加以下依赖:
xml
<!-- Spring Boot Data Redis(包含 Lettuce 客户端) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Cache 抽象层 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 连接池(Lettuce 需要 commons-pool2 才能启用连接池) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>三、application.yml 配置
yml
spring:
redis:
host: localhost
port: 6379
password: # 有密码则填写
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
cache:
type: redis
redis:
time-to-live: 3600s # 默认缓存过期时间
cache-null-values: true # 允许缓存 null,防止缓存穿透
use-key-prefix: true # 使用缓存名称作为 key 前缀
key-prefix: "app:" # 全局 key 前缀
server:
port: 8080四、CacheConfig 配置类
java
package com.example.demo.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* Spring Cache + Redis 完整配置类
* 继承 CachingConfigurerSupport 以自定义 KeyGenerator 和 CacheErrorHandler
*/
@Slf4j
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
// ==================== 1. 核心:CacheManager 配置 ====================
/**
* 主缓存管理器(默认使用)
* 支持为不同 cacheName 设置不同的过期时间
*/
@Bean
@Primary
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.builder(redisConnectionFactory)
// 默认缓存配置(1小时过期)
.cacheDefaults(getDefaultCacheConfig(Duration.ofHours(1)))
// 为特定缓存名称配置独立的过期时间
.withCacheConfiguration("userCache", getCacheConfigWithTtl(Duration.ofMinutes(30)))
.withCacheConfiguration("productCache", getCacheConfigWithTtl(Duration.ofHours(2)))
.withCacheConfiguration("dictCache", getCacheConfigWithTtl(Duration.ofDays(7)))
// 事务感知:在事务提交后才真正写入/删除缓存
.transactionAware()
.build();
}
/**
* 专门用于缓存 null 值的 CacheManager(防止缓存穿透)
* 使用方式:@Cacheable(cacheManager = "cacheNullManager", ...)
*/
@Bean
public CacheManager cacheNullManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(
getDefaultCacheConfig(Duration.ofMinutes(10))
// 允许缓存 null 值
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
)
)
.transactionAware()
.build();
}
// ==================== 2. RedisCacheConfiguration 配置 ====================
/**
* 默认缓存配置
*/
private RedisCacheConfiguration getDefaultCacheConfig(Duration duration) {
return RedisCacheConfiguration.defaultCacheConfig()
// Key 序列化:String
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
)
// Value 序列化:JSON(带类型信息,支持反序列化)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(createJsonSerializer())
)
// 设置缓存过期时间
.entryTtl(duration)
// 不缓存 null 值(默认配置)
.disableCachingNullValues()
// 使用缓存名称作为 key 前缀
.computePrefixWith(cacheName -> "app:" + cacheName + "::");
}
/**
* 获取指定 TTL 的缓存配置
*/
private RedisCacheConfiguration getCacheConfigWithTtl(Duration duration) {
return getDefaultCacheConfig(duration);
}
/**
* 创建支持类型信息的 JSON 序列化器
* 关键:必须保留类型信息,否则反序列化会丢失类型
*/
private GenericJackson2JsonRedisSerializer createJsonSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
// 激活默认类型信息,写入到 @class 属性中
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
// ==================== 3. 自定义 KeyGenerator ====================
/**
* 自定义 Key 生成策略
* 使用方式:@Cacheable(keyGenerator = "myKeyGenerator")
*/
@Override
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
// 类名
sb.append(target.getClass().getSimpleName());
sb.append(":");
// 方法名
sb.append(method.getName());
sb.append(":");
// 参数
if (params.length > 0) {
for (Object param : params) {
sb.append(param != null ? param.toString() : "null");
sb.append(":");
}
sb.deleteCharAt(sb.length() - 1); // 删除最后一个冒号
}
return sb.toString();
}
};
}
// ==================== 4. 缓存异常处理 ====================
/**
* 自定义缓存异常处理器
* 当 Redis 宕机或连接异常时,不影响业务正常执行
*/
@Override
@Bean
public CacheErrorHandler errorHandler() {
log.info("初始化 Redis CacheErrorHandler");
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.error("Redis 缓存读取异常,cache={}, key={}", cache.getName(), key, e);
// 不抛出异常,让方法继续执行(走数据库)
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.error("Redis 缓存写入异常,cache={}, key={}", cache.getName(), key, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
log.error("Redis 缓存删除异常,cache={}, key={}", cache.getName(), key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.error("Redis 缓存清空异常,cache={}", cache.getName(), e);
}
};
}
}配置说明:
| 配置项 | 说明 |
|---|---|
@EnableCaching | 让 Spring 扫描 @Cacheable、@CachePut、@CacheEvict 注解,并为之生成代理。 |
defaultCacheConfig() | 拿到一个默认配置原型,我们再定制。 |
entryTtl(Duration) | 缓存过期时间。这里默认 1 小时。 |
serializeKeysWith(...) | Key 序列化器。StringRedisSerializer 把 Key 存成普通字符串,在 Redis 里可读性好(如 weather:realtime::101280101)。 |
serializeValuesWith(...) | Value 序列化器。GenericJackson2JsonRedisSerializer 将对象转成 JSON,并附加 @class 字段用于反序列化还原类型。好处:对象无需实现 Serializable,且数据可读。 |
disableCachingNullValues() | 不缓存 null 结果。如果方法返回 null,直接跳过缓存写入。避免缓存空值导致穿透。 |
Map<String, RedisCacheConfiguration> | 针对不同缓存区域设置独立 TTL,名称与 @Cacheable(cacheNames=...) 对应。未匹配到的仍用 defaultConfig。 |
transactionAware() | 支持缓存操作感知 Spring 事务(仅当 Redis 事务开启时有效)。 |
withInitialCacheConfigurations(...) | 把上述 Map 注入管理器。 |
五、注解使用详解
5.1 基础注解
java
@Service
public class UserService {
/**
* @Cacheable: 先查缓存,命中则直接返回;未命中则执行方法并缓存结果
* value/cacheNames: 缓存名称(对应 Redis 中的 key 前缀)
* key: 缓存 key,支持 SpEL 表达式
* unless: 条件判断,满足条件时不缓存
* sync: 同步模式(防止缓存击穿,只有一个线程去查数据库)
*/
@Cacheable(value = "userCache", key = "#userId", unless = "#result == null", sync = true)
public User getUserById(Long userId) {
log.info("从数据库查询用户: {}", userId);
return userMapper.selectById(userId);
}
/**
* @CachePut: 先执行方法,再将结果放入缓存(常用于更新操作)
*/
@CachePut(value = "userCache", key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
/**
* @CacheEvict: 删除缓存
* allEntries = true: 清空该 cacheName 下所有缓存
* beforeInvocation: 方法执行前删除缓存(默认是执行后)
*/
@CacheEvict(value = "userCache", key = "#userId")
public void deleteUser(Long userId) {
userMapper.deleteById(userId);
}
/**
* @Caching: 组合多个缓存操作
*/
@Caching(
cacheable = @Cacheable(value = "userCache", key = "#phone"),
evict = @CacheEvict(value = "userListCache", allEntries = true)
)
public User getUserByPhone(String phone) {
return userMapper.selectByPhone(phone);
}
}注解参数详解:
cacheNames/value:指定缓存区域名称。这里与CacheConfig中 Map 的 key 完全一致,如"weather:realtime",会使用该区域专属 TTL。key:SpEL(Spring Expression Language)表达式,定义 缓存键。#keyword:取方法参数keyword的值。#locationId:取参数locationId。#request.remoteAddr:取HttpServletRequest对象的remoteAddr属性(IP 字符串)。
- 默认 key 生成:如果不写
key,Spring 会使用SimpleKeyGenerator基于所有参数生成 key。但明确指定更可控。
最终存入 Redis 的 Key 长什么样? 格式:缓存名::key值 例如 weather:realtime::101280101,在 Redis 中就是一个字符串键。
5.2 类级别注解 @CacheConfig
java
@Service
@CacheConfig(cacheNames = "userCache") // 类中所有缓存注解的默认 cacheNames
public class UserService {
@Cacheable(key = "#id") // 无需再写 value = "userCache"
public User getById(Long id) { ... }
@CachePut(key = "#user.id")
public User update(User user) { ... }
}5.3 使用自定义 KeyGenerator
java
@Service
public class ProductService {
// 使用自定义的 KeyGenerator
@Cacheable(value = "productCache", keyGenerator = "myKeyGenerator")
public Product getProduct(Long productId) {
return productMapper.selectById(productId);
}
}六、SpEL 表达式速查表
| 表达式 | 说明 | 示例 |
|---|---|---|
#root.methodName | 当前方法名 | key = "#root.methodName" |
#root.target | 当前被调用的对象 | |
#root.args[0] | 第一个参数 | key = "#root.args[0]" |
#paramName | 参数名(需编译带参数名) | key = "#userId" |
#p0, #a0 | 第一个参数(兼容写法) | key = "#p0" |
#result | 方法返回值(仅 unless/condition 可用) | unless = "#result == null" |
#root.caches[0].name | 缓存名称 |
七、多配置环境
7.1 核心要点
- 必须有一个
@PrimaryCacheManager 当注解未指定cacheManager时,Spring 会使用默认的(即标记了@Primary的)CacheManager。 - 注解显式指定
@Cacheable(cacheManager = "redisCacheManager")直接引用 Bean 名称。 - CacheResolver 更灵活 如果需要根据运行时条件动态选择,可实现
CacheResolver接口。 - 多个同类型 CacheManager 的 Bean 名称 自定义
@Bean方法名就是 Bean 名称,如redisCacheManager()生成名为redisCacheManager的 Bean。
7.2 Redis + Caffeine 两级缓存
java
@Configuration
@EnableCaching
public class MultiCacheConfig {
// ========== Redis 缓存管理器(默认) ==========
@Primary // 标记为默认
@Bean("redisCacheManager")
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
// ========== Caffeine 本地缓存管理器 ==========
@Bean("caffeineCacheManager")
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000));
// 注意:CaffeineCacheManager 默认自动创建缓存区域,无需预定义
return cacheManager;
}
}使用:
java
@Service
public class WeatherService {
// 使用 Redis 缓存(默认,不写 cacheManager 也可以)
@Cacheable(cacheNames = "weather:realtime", key = "#locationId")
public RealtimeVO realtime(String locationId) {
// ...
}
// 使用本地 Caffeine 缓存
@Cacheable(cacheNames = "hotCities", key = "#region", cacheManager = "caffeineCacheManager")
public List<City> hotCities(String region) {
// ...
}
}realtime方法未指定cacheManager,走@Primary即 Redis。hotCities显式指定cacheManager = "caffeineCacheManager",走本地内存。
7.3 多个 RedisCacheManager(不同配置)
假设你需要两套 Redis 缓存,一套用 JSON 序列化、一套用 JDK 序列化,或者连接不同 Redis 实例。
先定义两个 RedisConnectionFactory(略,通过 LettuceConnectionFactory 指向不同 host/port),然后:
java
@Configuration
@EnableCaching
public class MultiRedisCacheConfig {
@Autowired
@Qualifier("redisConnectionFactory1")
private RedisConnectionFactory factory1;
@Autowired
@Qualifier("redisConnectionFactory2")
private RedisConnectionFactory factory2;
@Primary
@Bean("jsonCacheManager")
public CacheManager jsonCacheManager() {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory1).cacheDefaults(config).build();
}
@Bean("jdkCacheManager")
public CacheManager jdkCacheManager() {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new JdkSerializationRedisSerializer()));
return RedisCacheManager.builder(factory2).cacheDefaults(config).build();
}
}使用方式同上,通过 cacheManager = "jdkCacheManager" 选择。
7.4 使用 CompositeCacheManager 组合
CompositeCacheManager 可以包含多个 CacheManager,查询时按顺序遍历,找到第一个包含目标缓存名称的返回结果。适合“本地 → 远程”的透明回退
java
@Configuration
@EnableCaching
public class CompositeCacheConfig {
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cm = new CaffeineCacheManager();
cm.setCaffeine(Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES));
return cm;
}
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
// ...标准配置
return RedisCacheManager.builder(connectionFactory).build();
}
@Primary
@Bean
public CacheManager compositeCacheManager(
@Qualifier("caffeineCacheManager") CacheManager caffeine,
@Qualifier("redisCacheManager") CacheManager redis) {
CompositeCacheManager composite = new CompositeCacheManager(caffeine, redis);
composite.setFallbackToNoOpCache(false); // 未命中时是否返回 NoOpCache
return composite;
}
}此时,所有 @Cacheable 默认走 compositeCacheManager。它会先用 Caffeine 查,没有则查 Redis。但不会自动回填,需要自己实现多级同步(通常是主动 @CachePut 到两级)。因此 CompositeCacheManager 更适合简单的兜底查询,真正多级缓存一般用专门框架(如 JetCache)更佳。
7.5 注意事项与最佳实践
- Bean 名称冲突 同类型
CacheManager的 Bean 名必须唯一,方法名即 Bean 名称。用@Bean("xxx")显式命名。 - 序列化一致性 同一个缓存名称(
cacheNames)的所有操作必须使用相同的序列化策略,否则反序列化会失败。 - 不要混用
cacheManager和cacheResolver二者互斥,选择一个。 - 事务感知 如果某些缓存需感知事务,可在对应
RedisCacheManager上调用.transactionAware()。 - 监控 多套
CacheManager时,监控各自的命中率、内存占用会更复杂,建议结合 Micrometer 等统一导出。 - 多 Redis 实例 如果连接不同 Redis,确保
RedisConnectionFactory分别注入,且排除自动配置的干扰(可定义@Primary工厂)。
7.6 总结
多套 CacheManager 的配置步骤:
- 定义多个
CacheManagerBean,用@Bean("name")指定唯一名称。 - 用
@Primary标记默认管理器。 - 在
@Cacheable/@CachePut/@CacheEvict中通过cacheManager属性指定使用哪一个。 - 如有动态选择需求,改用
CacheResolver。