Spring Boot Cache使用Redis自定义单个缓存名称过期时间
spring boot 使用redis作为缓存时,默认只提供了全局缓存过期时间,不能针对某一个缓存name单独设置缓存过期时间,如果需要精确设置每个缓存name 的过期时间,需要按照如下方式配置
1、 添加配置属性
spring:
cache:
cache-names: gxf
redis:
# 全局缓存过期时间
time-to-live: 10m
# 自定义单个缓存name过期时间
time-to-live:
# 单个name过期时间
cache1: 10m
# 多个name过期时间,key包含特殊符号需要"[]"包裹
"[cache1,cache2]": 10m
2、配置文件接收多个配置属性
注意:使用 @Value 注解不能同时接收多个属性,需要配合EL表达式;这里使用@ConfigurationProperties
/**
* Redis过期时间配置
* <pre>
* spring:
* cache:
* time-to-live:
* cache-name1: 10m
* # 多个key 请求使用逗号分割,并且使用"[]"包裹
* "[cache1,cache2]": 20h
* </pre>
*
*/
@Getter
@Setter
@ToString
@ConfigurationProperties(prefix = "spring.cache")
public class RedisTtlProperties {
/**
* 缓存过期时间
*/
private Map<String, Duration> timeToLive = new HashMap<>();
}
3、添加缓存配置类
@EnableCaching
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({RedisTtlProperties.class})
public class CacheConfig {
private static final Logger log = LoggerFactory.getLogger(CacheConfig.class);
/**
* 自定义redis缓存名称过期时间
* @return RedisCacheManagerBuilderCustomizer
*/
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer(RedisTtlProperties redisTtl) {
Map<String, Duration> timeToLiveMap = redisTtl.getTimeToLive();
if(MapUtils.isEmpty(timeToLiveMap)){
return null;
}
return (builder) -> {
for (Map.Entry<String,Duration> cacheTimeToLive: timeToLiveMap.entrySet()){
String[] keys = StringUtils.split(cacheTimeToLive.getKey(), ",");
for(String cache: keys){
builder.withCacheConfiguration(cache, RedisCacheConfiguration.defaultCacheConfig().entryTtl(cacheTimeToLive.getValue()));
}
}
};
}
}
接下来就可以直接使用@Cacheable注解了,单使用指定的cacheNames时会自动设置过期时间
4、缺点
以上配置添加完成之后,Redis 缓存默认的key 为 cacheName : key。这就会导致不同方法参数如果一致就会使用相同的缓存key。所以需要定制化Redis key 生成策略,只需要如下代码
@Configuration
@EnableCaching
@EnableConfigurationProperties({RedisTtlProperties.class})
public class RedisConfig extends CachingConfigurerSupport {
/**
* 设置Redis Key 缓存策略
*
* @return 结果
*/
@Override
@Bean("redisKeyGenerator")
public KeyGenerator keyGenerator() {
return new RedisKeyGenerator();
}
/**
* Redis Key 生成策略
*
* @author jonk
* @date 2022/12/26 23:42
*/
public static class RedisKeyGenerator implements KeyGenerator {
/**
* Generate a key for the given method and its parameters.
*
* @param target the target instance
* @param method the method being called
* @param params the method parameters (with any var-args expanded)
* @return a generated key
*/
@Override
public Object generate(Object target, Method method, Object... params) {
return String.format("%s:%s:%s",
method.getDeclaringClass().getSimpleName(),
method.getName(),
StringUtils.joinWith("_", params)
);
}
}
}
经过以上修改,虽然解决了缓存key生成问题,但是默认的缓存值使用Jdk序列化的,这回导致开发过程中不太清楚缓存到底存了什么,更不能修改缓存值。
5、修改Redis 缓存序列化方式
spring-boot-starter-data-redis 已经带了 jackson 序列化方式,我们直接使用就好;
代码如下:
@Bean
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
RedisSerializer<Object> redisSerializer = this.getRedisSerializer();
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(redisSerializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(redisSerializer);
template.afterPropertiesSet();
return template;
}
@SuppressWarnings(value = {"unchecked", "rawtypes"})
private RedisSerializer<Object> getRedisSerializer() {
ObjectMapper mapper = new ObjectMapper();
// 未知属性反序列化不报错
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
// null 值序列化配置
GenericJackson2JsonRedisSerializer.registerNullValueSerializer(mapper, null);
return new GenericJackson2JsonRedisSerializer(mapper);
}
上面配置解决了RedisTemplate 序列化方式采用Json格式写入,但对于采用@Cacheable 注解的序列化方式还是没有指定。
6、@Cacheable Redis指定json序列化方式
代码如下:
使用application.yml 配置生成标准的RedisCacheConfiguration
/**
* 自定义RedisCacheConfiguration 设置值序列化方式,改配置会覆盖默认的缓存配置
*
* @param cacheProperties 缓存属性
* @return RedisCacheConfiguration
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(SerializationPair.fromSerializer(this.getRedisSerializer()));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
/**
* 自定义redis缓存名称过期时间
*
* @return RedisCacheManagerBuilderCustomizer
*/
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer(RedisCacheConfiguration redisCacheConfiguration, RedisTtlProperties redisTtl) {
return (builder) -> {
// 自定义缓存过期时间,类中已经初始化
redisTtl.getTimeToLives().forEach((k, v) -> {
Arrays.stream(StringUtils.split(k, ",")).forEach(cache -> {
builder.withCacheConfiguration(cache, redisCacheConfiguration.entryTtl(v));
});
});
};
}
经过以上几步,基本解决Redis 缓存中使用序列化问题,全代码概览
package com.ruoyi.framework.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.framework.config.properties.RedisTtlProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* redis配置
*
* @author ruoyi
*/
@Configuration
@EnableCaching
@EnableConfigurationProperties({RedisTtlProperties.class})
public class RedisConfig extends CachingConfigurerSupport {
@Bean
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
RedisSerializer<Object> redisSerializer = this.getRedisSerializer();
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(redisSerializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(redisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 自定义RedisCacheConfiguration 设置值序列化方式,改配置会覆盖默认的缓存配置
*
* @param cacheProperties 缓存属性
* @return RedisCacheConfiguration
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(SerializationPair.fromSerializer(this.getRedisSerializer()));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
/**
* 自定义redis缓存名称过期时间
*
* @return RedisCacheManagerBuilderCustomizer
*/
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer(RedisCacheConfiguration redisCacheConfiguration, RedisTtlProperties redisTtl) {
return (builder) -> {
// 自定义缓存过期时间,类中已经初始化
redisTtl.getTimeToLives().forEach((k, v) -> {
Arrays.stream(StringUtils.split(k, ",")).forEach(cache -> {
builder.withCacheConfiguration(cache, redisCacheConfiguration.entryTtl(v));
});
});
};
}
/**
* 设置Redis Key 缓存策略
*
* @return 结果
*/
@Override
@Bean("redisKeyGenerator")
public KeyGenerator keyGenerator() {
return new RedisKeyGenerator();
}
@SuppressWarnings(value = {"unchecked", "rawtypes"})
private RedisSerializer<Object> getRedisSerializer() {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
GenericJackson2JsonRedisSerializer.registerNullValueSerializer(mapper, null);
return new GenericJackson2JsonRedisSerializer(mapper);
}
/**
* Redis Key 生成策略
*
* @author jonk
* @date 2022/12/26 23:42
*/
public static class RedisKeyGenerator implements KeyGenerator {
/**
* Generate a key for the given method and its parameters.
*
* @param target the target instance
* @param method the method being called
* @param params the method parameters (with any var-args expanded)
* @return a generated key
*/
@Override
public Object generate(Object target, Method method, Object... params) {
return String.format("%s:%s:%s",
method.getDeclaringClass().getSimpleName(),
method.getName(),
StringUtils.joinWith("_", params)
);
}
}
}