(1). 前言
在前面说过,Spring Cache与Redis结合时,所有key的过期时间(TTL)是统一的(可能引起缓存雪崩问题),如果想要做到某个key,有自己的过期时间,就只能自己去扩展源码了.
(2). 实现方式
- 把过期时间cacheName放在一起(@Cacheable(cacheNames = “users#PT60s”)).
- 自定义: 注解(@Cacheable(cacheNames = “users”, ttl = “PT30s”)).
- 自定义配置RedisCacheManager构建过程,因为:RedisCacheManager内部有一个属性(Map<String, RedisCacheConfiguration>),Key就是cacneName.
在这里,我这里使用:方案一,方案二已经开源在github
(3). 自定义CacheManager
直接把RedisCacheManager拷贝过来,重命名,注意:package名称不能动,因为,在这个类的内部使用到了其它包级别的类(注意:标星的部位是我改动的部位,其余地方不变即可).
package org.springframework.data.redis.cache;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
public class RedisCacheNewManager extends AbstractTransactionSupportingCacheManager {
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
private final Map<String, RedisCacheConfiguration> initialCacheConfiguration;
private final boolean allowInFlightCacheCreation;
private RedisCacheNewManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
boolean allowInFlightCacheCreation) {
Assert.notNull(cacheWriter, "CacheWriter must not be null!");
Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null!");
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
this.initialCacheConfiguration = new LinkedHashMap<>();
this.allowInFlightCacheCreation = allowInFlightCacheCreation;
}
public RedisCacheNewManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
this(cacheWriter, defaultCacheConfiguration, true);
}
public RedisCacheNewManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
String... initialCacheNames) {
this(cacheWriter, defaultCacheConfiguration, true, initialCacheNames);
}
public RedisCacheNewManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
boolean allowInFlightCacheCreation, String... initialCacheNames) {
this(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation);
for (String cacheName : initialCacheNames) {
this.initialCacheConfiguration.put(cacheName, defaultCacheConfiguration);
}
}
public RedisCacheNewManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
this(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, true);
}
public RedisCacheNewManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
this(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation);
Assert.notNull(initialCacheConfigurations, "InitialCacheConfigurations must not be null!");
this.initialCacheConfiguration.putAll(initialCacheConfigurations);
}
public static RedisCacheNewManager create(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return new RedisCacheNewManager(new DefaultRedisCacheWriter(connectionFactory),
RedisCacheConfiguration.defaultCacheConfig());
}
public static RedisCacheManagerBuilder builder(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory);
}
public static RedisCacheManagerBuilder builder(RedisCacheWriter cacheWriter) {
Assert.notNull(cacheWriter, "CacheWriter must not be null!");
return RedisCacheManagerBuilder.fromCacheWriter(cacheWriter);
}
@Override
protected Collection<RedisCache> loadCaches() {
List<RedisCache> caches = new LinkedList<>();
for (Map.Entry<String, RedisCacheConfiguration> entry : initialCacheConfiguration.entrySet()) {
caches.add(createRedisCache(entry.getKey(), entry.getValue()));
}
return caches;
}
@Override
protected RedisCache getMissingCache(String name) {
return allowInFlightCacheCreation ? createRedisCache(name, defaultCacheConfig) : null;
}
public Map<String, RedisCacheConfiguration> getCacheConfigurations() {
Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(getCacheNames().size());
getCacheNames().forEach(it -> {
RedisCache cache = RedisCache.class.cast(lookupCache(it));
configurationMap.put(it, cache != null ? cache.getCacheConfiguration() : null);
});
return Collections.unmodifiableMap(configurationMap);
}
// ********************************************************************************
// 1. 判断名字是否包含有指定字符(#)
// 其实,这里可以做一个回调函数,扔出去给外部使用.比如:
/ 可以,把name和ttl放在配置文件中,读取配置文件即可.
// ********************************************************************************
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
RedisCacheConfiguration tmpCacheConfig = cacheConfig != null ? cacheConfig : defaultCacheConfig;
String tmpName = name;
int position = name.indexOf("#");
if (position > 0) {
// 缓存名称
tmpName = name.substring(0, position);
// 过期时间
String ttl = name.substring(position + 1);
// 重新设置过期时间(内部实际是重新new出了一个RedisCacheConfiguration对象)
tmpCacheConfig = tmpCacheConfig.entryTtl(Duration.parse(ttl));
}
return new RedisCache(tmpName, cacheWriter, tmpCacheConfig);
}
public static class RedisCacheManagerBuilder {
private final RedisCacheWriter cacheWriter;
private RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
private final Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>();
private boolean enableTransactions;
boolean allowInFlightCacheCreation = true;
private RedisCacheManagerBuilder(RedisCacheWriter cacheWriter) {
this.cacheWriter = cacheWriter;
}
public static RedisCacheManagerBuilder fromConnectionFactory(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
return builder(new DefaultRedisCacheWriter(connectionFactory));
}
public static RedisCacheManagerBuilder fromCacheWriter(RedisCacheWriter cacheWriter) {
Assert.notNull(cacheWriter, "CacheWriter must not be null!");
return new RedisCacheManagerBuilder(cacheWriter);
}
public RedisCacheManagerBuilder cacheDefaults(RedisCacheConfiguration defaultCacheConfiguration) {
Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null!");
this.defaultCacheConfiguration = defaultCacheConfiguration;
return this;
}
public RedisCacheManagerBuilder transactionAware() {
this.enableTransactions = true;
return this;
}
public RedisCacheManagerBuilder initialCacheNames(Set<String> cacheNames) {
Assert.notNull(cacheNames, "CacheNames must not be null!");
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>(cacheNames.size());
cacheNames.forEach(it -> cacheConfigMap.put(it, defaultCacheConfiguration));
return withInitialCacheConfigurations(cacheConfigMap);
}
public RedisCacheManagerBuilder withInitialCacheConfigurations(
Map<String, RedisCacheConfiguration> cacheConfigurations) {
Assert.notNull(cacheConfigurations, "CacheConfigurations must not be null!");
cacheConfigurations.forEach((cacheName, configuration) -> Assert.notNull(configuration,
String.format("RedisCacheConfiguration for cache %s must not be null!", cacheName)));
this.initialCaches.putAll(cacheConfigurations);
return this;
}
public RedisCacheManagerBuilder disableCreateOnMissingCache() {
this.allowInFlightCacheCreation = false;
return this;
}
// ********************************************************************************
// 2. 通过Builder对象,返回的是一个新的:RedisCacheNewManager
// ********************************************************************************
public RedisCacheNewManager build() {
RedisCacheNewManager cm = new RedisCacheNewManager(cacheWriter, defaultCacheConfiguration, initialCaches,
allowInFlightCacheCreation);
cm.setTransactionAware(enableTransactions);
return cm;
}
}
}
(4). 配置自定义的CacheManager
package help.lixin.config;
import java.util.LinkedHashSet;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.cache.CacheProperties.Redis;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.redis.cache.RedisCacheNewManager;
import org.springframework.data.redis.cache.RedisCacheNewManager.RedisCacheManagerBuilder;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class RedisConnectionFactoryConfig {
@Autowired
private RedisProperties redisProperties;
@Autowired
private CacheProperties cacheProperties;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
List<String> clusterNodes = redisProperties.getCluster().getNodes();
String passwordAsString = redisProperties.getPassword();
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(clusterNodes);
redisClusterConfiguration.setPassword(RedisPassword.of(passwordAsString));
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空闲连接数, 默认8个
jedisPoolConfig.setMaxIdle(100);
// 最大连接数, 默认8个
jedisPoolConfig.setMaxTotal(500);
// 最小空闲连接数, 默认0
jedisPoolConfig.setMinIdle(0);
// 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,
// 默认-1
jedisPoolConfig.setMaxWaitMillis(2000); // 设置2秒
// 对拿到的connection进行validateObject校验
jedisPoolConfig.setTestOnBorrow(true);
return new JedisConnectionFactory(redisClusterConfiguration, jedisPoolConfig);
}
// ********************************************
// 1. 使用自定义的RedisCacheNewManager
// ********************************************
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheNewManager
.builder(redisConnectionFactory)
.cacheDefaults(determineConfiguration(resourceLoader.getClassLoader()));
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
return builder.build();
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
ClassLoader classLoader) {
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
// json序列化
config = config.serializeValuesWith(SerializationPair.fromSerializer(RedisSerializer.json()));
// # spring.cache.redis.time-to-live=PT60s
// 在配置文件中,指定的缓存过期时间,代表的是所有KEY的过期时间.
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
(5). 测试验证
127.0.0.1:6382> keys *
1) "users::1"
127.0.0.1:6382> GET "users::1"
"[\"java.util.ArrayList\",[{\"@class\":\"help.lixin.entity.User\",\"id\":1,\"name\":\"\xe5\xbc\xa0\xe4\xb8\x89\"},{\"@class\":\"help.lixin.entity.User\",\"id\":2,\"name\":\"\xe6\x9d\x8e\xe5\x9b\x9b\"},{\"@class\":\"help.lixin.entity.User\",\"id\":3,\"name\":\"\xe7\x8e\x8b\xe4\xba\x94\"},{\"@class\":\"help.lixin.entity.User\",\"id\":4,\"name\":\"\xe8\xb5\xb5\xe5\x85\xad\"}]]"
# 查看TTL过期时间
127.0.0.1:6382> TTL "users::1"
(integer) 41
(6). 总结
通过简单的改造,就可以实现针对某一key配置过期时间.实际,像缓存穿透(布隆过滤器),还是需要进一步对:CacheInterceptor进行扩展的.