Spring cache + Redis 实现缓存时,在设置TTL的过期时间需要针对每个容器单独编码设置过期时间:
@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {
private static Logger logger = LoggerFactory.getLogger(RedisCacheConfig.class);
public static final String OUTBOUNDLINE_CACHE = "OUTBOUNDLINE_CACHE";
public static final String THIRDAGENT_CACHE = "THIRDAGENT_CACHE";
public static final String PLATFORM_CACHE = "PLATFORM_CACHE";
public static final String CHECKIN_AGENT_CACHE = "CHECKIN_AGENT_CACHE";
@Bean
CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
//设置不同cacheName的过期时间
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>(16);
cacheConfigurations.put(OUTBOUNDLINE_CACHE, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30L)));
cacheConfigurations.put(PLATFORM_CACHE, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30L)));
cacheConfigurations.put(CHECKIN_AGENT_CACHE, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30L)));
cacheConfigurations.put(THIRDAGENT_CACHE, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30L)));
return RedisCacheManager.builder(RedisCacheWriter.lockingRedisCacheWriter
(connectionFactory)).withInitialCacheConfigurations(cacheConfigurations).transactionAware().build();
}
}
采用该方法可以满足一定的需求,但是使用上不够灵活。类比Redis 单独实现缓存,可以针对 每个key值设置特定的TTL值,从而实现个性化key设置过期时间;
Cache 如何实现如下个性化注解式TTL?
// 含义: 设置当前cacheTest 存储容器的过期时间为60s
@Cacheable(value = "cacheTest#60",key = "#root.methodName")
通过调研,多数都是基于spring boot 1.x 系列实现的,是无法满足spring boot2.x 系列Cache 实现TTL;因为在spring boot 2.x 系列的RedisCacheManager 源码中是没有setExpires 设置ttl 的方法的;
public Cache getCache(String name) {
// Quick check for existing cache...
Cache cache = this.cacheMap.get(name);
if (cache != null) {
return cache;
}
// The provider may support on-demand cache creation...
Cache missingCache = getMissingCache(name);
if (missingCache != null) {
// Fully synchronize now for missing cache registration
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = decorateCache(missingCache);
this.cacheMap.put(name, cache);
updateCacheNames(name);
}
}
}
return cache;
}
// 查询cache 中的结果集
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper result = get(key);
if (result != null) {
return (T) result.get();
}
T value = valueFromLoader(key, valueLoader);
put(key, value);
return value;
}
// cache 进行put 结果集
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
if (!isAllowNullValues() && cacheValue == null) {
throw new IllegalArgumentException(String.format(
"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
name));
}
cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
}
package com.corn.redis.config;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
/**
* author:zj
* Date:2020/4/8
* Time:14:55
* 通过重写RedisCache中的put的方法 实现spring cache 自定义key ttl事件
*
*/
public class CustomizeRedisCache extends RedisCache {
private RedisCacheWriter redisCacheWriter;
private RedisCacheConfiguration configuration;
//校验规则:获取时间
String REGEX_STR = ".*\\#\\d+$";
private static final String Splitter="#";
/**
* Create new {@link RedisCache}.
*
* @param name must not be {@literal null}.
* @param cacheWriter must not be {@literal null}.
* @param cacheConfig must not be {@literal null}.
*/
protected CustomizeRedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(name, cacheWriter, cacheConfig);
redisCacheWriter =cacheWriter;
configuration =cacheConfig;
}
/**
* 重写cache put 逻辑,引入自定义TTL 实现
* 实现逻辑:
* 1.通过获取@Cacheable 中的value ,然后根据约定好的特殊字符进行分割
* 2.从分割结果集中获取设置的TTL 时间并将value 中的,然后给当前缓存设置TTL
*
* @param key
* @param value
*/
@Override
public void put(Object key, Object value) {
String name = super.getName();
//是否按照指定的格式
if (Pattern.matches(REGEX_STR, name)) {
List<String> keyList = Arrays.asList(name.split(Splitter));
//获取键值
String finalName = keyList.get(0);
//获取TTL 执行时间
Long ttl = Long.valueOf(keyList.get(1));
//获取缓存value
Object cacheValue = preProcessCacheValue(value);
//获取value 为null 时,抛出异常
if (!isAllowNullValues() && cacheValue == null) {
throw new IllegalArgumentException(String.format(
"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
name));
}
//插入时添加时间
redisCacheWriter.put(finalName, serializeCacheKey(createCacheKey(key)), serializeCacheValue(cacheValue), Duration.ofSeconds(ttl));
} else {
//原来逻辑处理
super.put(key, value);
}
}
/**
*@描述 现有key 值格式为 key#ttl ;改方法将key 值后边的#ttl 去掉 ;例如test# 10;改方法处理后为test
*@参数
*@返回值
*@创建人 zj
*@创建时间 2020/4/8
*/
protected String createCacheKey(Object key) {
String convertedKey = convertKey(key);
if (!configuration.usePrefix()) {
return convertedKey;
}
return prefixCacheKey(convertedKey);
}
private String prefixCacheKey(String key) {
String name = super.getName();
if (Pattern.matches(REGEX_STR, name)) {
List<String> keyList = Arrays.asList(name.split(Splitter));
String finalName = keyList.get(0);
return configuration.getKeyPrefixFor(finalName) + key;
}
// allow contextual cache names by computing the key prefix on every call.
return configuration.getKeyPrefixFor(name) + key;
}
}
重写RedisCacheManager
package com.corn.redis.config;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
/**
* author:zj
* Date:2020/4/8
* Time:15:01
*/
public class MyRedisCacheManager extends RedisCacheManager {
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
/** 用于返回自定义的redisCache **/
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
return new CustomizeRedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}
public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}
}
调用方式:
package com.corn.redis.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
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.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* author:zj
* Date:2020/4/3
* Time:16:06
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
@Override
public KeyGenerator keyGenerator() {
return (o, method, objects) -> {
StringBuilder sb = new StringBuilder();
sb.append(o.getClass().getName()).append(".");
sb.append(method.getName()).append(".");
for (Object obj : objects) {
sb.append(obj.toString());
}
System.out.println("keyGenerator=" + sb.toString());
return sb.toString();
};
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter =RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
return new MyRedisCacheManager(redisCacheWriter,redisCacheConfiguration);
}
}
单元测试:
/**
* 将结果集缓存,当结果集已缓存,从缓存中获取结果集,一般长用于数据删除
* @return
*/
@Cacheable(value = "cacheTest#60",key = "#root.methodName")
public List<String> getRedisCache(){
List<String> list = new ArrayList<>();
list.add("test");
return list;
}