起因
为了防止缓存雪崩,缓存过期时间最好设置成随机过期。
原始方案
public UserInfoVO getUserInfo(Long id) {
// 判断redis中是否存在缓存
String key = "userInfo:" + id;
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))){
return (UserInfoVO) redisTemplate.opsForValue().get(key);
}
if (id == null){
throw new RequestExcetption(MessageConstant.ILLEGAL_REQUEST);
}
UserInfoDO userInfoDO = userInfoService.getOne(new QueryWrapper<UserInfoDO>().eq("user_id", id));
if (userInfoDO == null || userInfoDO.getDeleted() == 1){
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
UserInfoVO userInfoVO = new UserInfoVO();
BeanUtils.copyProperties(userInfoDO, userInfoVO);
// 装入缓存,设置随机时间
Random random = new Random();
redisTemplate.opsForValue().set(key, userInfoVO, 10 + random.nextInt(11), TimeUnit.SECONDS);
return userInfoVO;
}
比如这里我想把用户信息数据缓存到Redis中,需要利用到RedisTemplate,为每个不同id的数据做key的拼接,然后用Random设置随机过期时间(如上是10-20分钟)。
并且因为采用旁路缓存模式,在更新数据后,同样需要对不同id做key的拼接,然后删除Redis中对应的key。如果是分页查询,那可能还需要设计一个哈希表来存储对应类别中的所有已缓存key,甚是麻烦。
因此考虑优化。
替换方案
@Cacheable(value = "userInfo", cacheManager = "redisCacheManager", key = "#id")
public UserInfoVO getUserInfo(Long id) {
if (id == null){
throw new RequestExcetption(MessageConstant.ILLEGAL_REQUEST);
}
UserInfoDO userInfoDO = userInfoService.getOne(new QueryWrapper<UserInfoDO>().eq("user_id", id));
if (userInfoDO == null || userInfoDO.getDeleted() == 1){
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
UserInfoVO userInfoVO = new UserInfoVO();
BeanUtils.copyProperties(userInfoDO, userInfoVO);
return userInfoVO;
}
直接采用SpringCache,通过注解的形式让框架自动完成缓存处理。
新的问题
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory){
return RedisCacheManager.builder(factory)
.withCacheConfiguration("user", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(2)))
.build();
}
SpringCache需要CacheManager对缓存做配置,如上所示创建一个RedisCacheManager的Bean对象,但问题在于配置中entryTtl只能传入Duration类设置为定值,无法随机化。
解决
Duration
public final class Duration implements TemporalAmount, Comparable<Duration>, Serializable {
// 省略
}
首先没法继承Duration修改里面的方法,因为该类被final修饰。
entryTtl方法
public RedisCacheConfiguration entryTtl(Duration ttl) {
Assert.notNull(ttl, "TTL duration must not be null!");
return new RedisCacheConfiguration(ttl, this.cacheNullValues, this.usePrefix, this.keyPrefix, this.keySerializationPair, this.valueSerializationPair, this.conversionService);
}
那从entryTtl方法出发,发现其作用是对当前类RedisCacheConfiguration的ttl属性赋值,那我是不是可以直接继承RedisCacheConfiguration类,然后改写这个方法,每次为ttl赋值一个随机数呢?
RedisCacheConfiguration类
public class RedisCacheConfiguration {
private final Duration ttl;
private final boolean cacheNullValues;
private final CacheKeyPrefix keyPrefix;
private final boolean usePrefix;
private final RedisSerializationContext.SerializationPair<String> keySerializationPair;
private final RedisSerializationContext.SerializationPair<Object> valueSerializationPair;
private final ConversionService conversionService;
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, RedisSerializationContext.SerializationPair<String> keySerializationPair, RedisSerializationContext.SerializationPair<?> valueSerializationPair, ConversionService conversionService) {
this.ttl = ttl;
this.cacheNullValues = cacheNullValues;
this.usePrefix = usePrefix;
this.keyPrefix = keyPrefix;
this.keySerializationPair = keySerializationPair;
this.valueSerializationPair = valueSerializationPair;
this.conversionService = conversionService;
}
// 省略代码
}
结果是也不能继承RedisCacheConfiguration类l,因为构造方法为私有,无法继承。
那换个思路,我什么时候需要用到ttl?
RedisCacheManager类
public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
private final Map<String, RedisCacheConfiguration> initialCacheConfiguration;
private final boolean allowInFlightCacheCreation;
// 省略...
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
return new RedisCache(name, this.cacheWriter, cacheConfig != null ? cacheConfig : this.defaultCacheConfig);
}
// 省略...
}
RedisCacheManager类初始化需要传入RedisCacheConfiguration配置,而在其createRedisCache方法中用到了这个配置,然后创建了一个RedisCache类。
RedisCache类
public class RedisCache extends AbstractValueAdaptingCache {
private static final byte[] BINARY_NULL_VALUE;
private final String name;
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration cacheConfig;
private final ConversionService conversionService;
protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(cacheConfig.getAllowCacheNullValues());
Assert.notNull(name, "Name must not be null!");
Assert.notNull(cacheWriter, "CacheWriter must not be null!");
Assert.notNull(cacheConfig, "CacheConfig must not be null!");
this.name = name;
this.cacheWriter = cacheWriter;
this.cacheConfig = cacheConfig;
this.conversionService = cacheConfig.getConversionService();
}
// 省略...
public void put(Object key, @Nullable Object value) {
Object cacheValue = this.preProcessCacheValue(value);
if (!this.isAllowNullValues() && cacheValue == null) {
// 抛出异常省略...
} else {
this.cacheWriter.put(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.cacheConfig.getTtl());
}
}
public Cache.ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
Object cacheValue = this.preProcessCacheValue(value);
if (!this.isAllowNullValues() && cacheValue == null) {
return this.get(key);
} else {
byte[] result = this.cacheWriter.putIfAbsent(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.cacheConfig.getTtl());
return result == null ? null : new SimpleValueWrapper(this.fromStoreValue(this.deserializeCacheValue(result)));
}
}
// 省略...
}
进入RedisCache,可以发现其put和putIfAbsent两个方法使用了配置的getTtl方法,用于写入缓存的过期时间。
并且RedisCache的构造方法并没有私有化,因此可以继承,然后重写put和putIfAbsent两个方法。
继承RedisCache类
public class RandomTtlRedisCache extends RedisCache {
/**
* 最小ttl,单位毫秒
*/
private int minTtl;
/**
* 最大ttl,单位毫秒
*/
private int maxTtl;
private Random random = new Random();
private final String name;
private final RedisCacheWriter cacheWriter;
protected RandomTtlRedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig, int minTtl, int maxTtl) {
super(name, cacheWriter, cacheConfig);
this.minTtl = minTtl;
this.maxTtl = maxTtl;
this.name = name;
this.cacheWriter = cacheWriter;
}
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = this.preProcessCacheValue(value);
if (!this.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.", this.name));
} else {
this.cacheWriter.put(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.getRandomTtl());
}
}
@Override
public Cache.ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
Object cacheValue = this.preProcessCacheValue(value);
if (!this.isAllowNullValues() && cacheValue == null) {
return this.get(key);
} else {
byte[] result = this.cacheWriter.putIfAbsent(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.getRandomTtl());
return result == null ? null : new SimpleValueWrapper(this.fromStoreValue(this.deserializeCacheValue(result)));
}
}
private Duration getRandomTtl() {
int randomTtl = minTtl + random.nextInt(maxTtl - minTtl + 1);
return Duration.ofMillis(randomTtl);
}
private byte[] createAndConvertCacheKey(Object key) {
return this.serializeCacheKey(this.createCacheKey(key));
}
}
如上创建了一个RandomTtlRedisCache类继承RedisCache。如何调用这个类呢,自然是回到RedisCacheManager类中的createRedisCache方法。
同理,可以创建一个RandomTtlRedisCacheManager类继承RedisCacheManager。
继承RedisCacheManager类
public class RandomTtlRedisCacheManager extends RedisCacheManager{
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
private final int minTtl;
private final int maxTtl;
public RandomTtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, int minTtl, int maxTtl) {
super(cacheWriter, defaultCacheConfiguration);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
this.minTtl = minTtl;
this.maxTtl = maxTtl;
}
@Override
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
return new RandomTtlRedisCache(name, this.cacheWriter, cacheConfig != null ? cacheConfig : this.defaultCacheConfig, minTtl, maxTtl);
}
}
至此,基本完成修改,只需要创建一个RandomTtlRedisCacheManager的Bean对象即可。
创建Bean对象
@Bean
public RandomTtlRedisCacheManager redisCacheManager(RedisConnectionFactory factory){
RedisCacheWriter cacheWriter = RedisCacheWriter.lockingRedisCacheWriter(factory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
return new RandomTtlRedisCacheManager(cacheWriter, redisCacheConfiguration, 30000, 120000);
}
比如这里设置了一个随机过期时间为30-120s的缓存配置。
对比优化
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory){
return RedisCacheManager.builder(factory)
.withCacheConfiguration("user", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(2)))
.build();
}
这是之前的RedisCacheManager,可以发现是可以设置缓存名称的,对应@Cacheable中的value属性,也就是说不同的value可以设置不同的配置。
但是新创建的RandomTtlRedisCacheManager只能设置一个基础配置,如何解决?
继续看源码。
RedisCacheManager
public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
private final Map<String, RedisCacheConfiguration> initialCacheConfiguration;
private final boolean allowInFlightCacheCreation;
public RedisCacheManager(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);
}
protected Collection<RedisCache> loadCaches() {
List<RedisCache> caches = new LinkedList();
Iterator var2 = this.initialCacheConfiguration.entrySet().iterator();
while(var2.hasNext()) {
Map.Entry<String, RedisCacheConfiguration> entry = (Map.Entry)var2.next();
caches.add(this.createRedisCache((String)entry.getKey(), (RedisCacheConfiguration)entry.getValue()));
}
return caches;
}
// builder
public static class RedisCacheManagerBuilder {
@Nullable
private RedisCacheWriter cacheWriter;
private RedisCacheConfiguration defaultCacheConfiguration;
private final Map<String, RedisCacheConfiguration> initialCaches;
private boolean enableTransactions;
boolean allowInFlightCacheCreation;
public RedisCacheManager build() {
// Assert断言省略...
RedisCacheManager cm = new RedisCacheManager(this.cacheWriter, this.defaultCacheConfiguration, this.initialCaches, this.allowInFlightCacheCreation);
cm.setTransactionAware(this.enableTransactions);
return cm;
}
}
}
回到RedisCacheManager,builder创建了一个叫initialCaches的Map,在build后执行RedisCacheManager的构造方法,将其值传入initialCacheConfiguration。
而initialCacheConfiguration在loadCaches方法中被使用,针对每个名称和配置,均创建一个独立的cache,然后返回一个cache集合。
最终方案
因此,可以在RandomTtlRedisCacheManager继续重写loadCaches方法,并做优化。
public class RandomTtlRedisCacheManager extends RedisCacheManager{
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
private final int minTtl;
private final int maxTtl;
private final Map<String, int[]> ttlConfigs;
public RandomTtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, int minTtl, int maxTtl, Map<String, int[]> ttlConfigs) {
super(cacheWriter, defaultCacheConfiguration);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
this.minTtl = minTtl;
this.maxTtl = maxTtl;
this.ttlConfigs = ttlConfigs;
}
@Override
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
return new RandomTtlRedisCache(name, this.cacheWriter, cacheConfig != null ? cacheConfig : this.defaultCacheConfig, minTtl, maxTtl);
}
@Override
protected Collection<RedisCache> loadCaches() {
List<RedisCache> caches = new LinkedList<>();
ttlConfigs.forEach((name, ttlArr) -> caches.add(new RandomTtlRedisCache(name, cacheWriter, defaultCacheConfig, ttlArr[0], ttlArr[1])));
return caches;
}
}
通过构造方法传入ttlConfigs,key为cache的名称,value为大小为2的int数组,对应最小最大随机时间,loadCaches遍历map,返回cache集合。
@Bean
public RandomTtlRedisCacheManager redisCacheManager(RedisConnectionFactory factory){
RedisCacheWriter cacheWriter = RedisCacheWriter.lockingRedisCacheWriter(factory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
// userInfo 缓存设置15 - 20分钟
// article 缓存设置30 - 40分钟
HashMap<String, int[]> ttlConfigs = new HashMap<>();
ttlConfigs.put("userInfo", new int[]{900000, 1200000});
ttlConfigs.put("article", new int[]{1800000, 2400000});
return new RandomTtlRedisCacheManager(cacheWriter, redisCacheConfiguration, 30000, 120000, ttlConfigs);
}
RandomTtlRedisCacheManager这个Bean对象中,创建缓存ttl映射map,调用RandomTtlRedisCacheManager构造方法。
上图例子:
任何采用了上述redisCacheManage作为CacheManager的缓存注解,
名为userInfo缓存设置15-20分钟随机过期时间,
名为article缓存设置30-40分钟过期随机,
而其他缓存则为30-120s的随机过期时间。