一、背景
我们在项目当中经常要使用redis作为缓存,可选择的方式有:1)编程方式 2)注解方式
1. 编程方式
- jedis
- redisTempate
- redisson
- …
2. 注解方式
- spring cache系列注解
- 通过切面自定义注解实现
由于spring @Cacheable提供了多种缓存实现,有的缓存是不支持ttl的,因此@Cacheable注解当中是没有设置ttl参数的,Redis实现的Cache,配置ttl必须单独集中配置,如下:
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultConfig);
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
// 自定义每个cacheName的配置
RedisCacheConfiguration config = cacheDefault(resourceLoader);
config = config.entryTtl(Duration.ofMinutes(30));
cacheConfigurations.put("store", config);
cacheConfigurations.put("store-nearby", defaultConfig);
return builder.withInitialCacheConfigurations(cacheConfigurations).build();
这样做有spring的道理,但是确实感觉不是很灵活,团队之前也基于切面实现了一套支持ttl的注解。最近项目架构升级到spring-boot,能不能基于spring-boot-data-redis扩展实现注解方式的ttl?
二、思路
1. 增加切面
比如,我们自定义一个@Ttl注解,如果一个方法上面同时有@Cacheable @Ttl注解,我们解析对应Redis Key, 方法执行结束后,给对应的key设置过期时间
2. 找SpringCache扩展点
Debug代码,寻找扩展点进行扩展。
经过比较,我发现spring生成key的策略是比较复杂的,RedisCacheConfiguration可以配置前缀,KeyGenerator又可以自定义配置key的生成策略。实现代码解析key难度较大,并且容易出现bug,直接放弃这种方式。因为我们要通过反射拿到方法上的ttl时间,我们这个扩展点首先要能获取当前执行的Method,奔着目标,debug代码,我把目标放在了RedisCache、CacheAspectSupport、CacheResolver、RedisCacheManager这几个类上。
RedisCache
@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());
}
重点代码
cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
这一行代码,我可以看出来,ttl是从全局变量cacheConfig的getTtl()获取的,下一步我要找到RedisCache的实例是什么时候生成并注入cacheConfig的。
CacheAspectSupport
protected Collection<? extends Cache> getCaches(
CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
if (caches.isEmpty()) {
throw new IllegalStateException("No cache could be resolved for '" +
context.getOperation() + "' using resolver '" + cacheResolver +
"'. At least one cache should be provided per cache operation.");
}
return caches;
}
CacheOperationInvocationContext这个类可以获取到Method, 获取Cache的操作又托管给了CacheResolver.resolveCaches方法
CacheResolver
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Collection<String> cacheNames = getCacheNames(context);
if (cacheNames == null) {
return Collections.emptyList();
}
Collection<Cache> result = new ArrayList<>(cacheNames.size());
for (String cacheName : cacheNames) {
Cache cache = getCacheManager().getCache(cacheName);
if (cache == null) {
throw new IllegalArgumentException("Cannot find cache named '" +
cacheName + "' for " + context.getOperation());
}
result.add(cache);
}
return result;
}
Cache cache = getCacheManager().getCache(cacheName);
getCache最终会调用RedisCacheManager
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}
这个方法是protected,我们在CacheResolver无法调用,只能自定义一个RedisCacheManager子类,实现一个createRedisCache方法。
三、实现
1. 注解
@CacheableTtl
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Cacheable(cacheResolver = "ttlCacheResolver")
public @interface CacheableTtl {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String keyGenerator() default "";
String condition() default "";
String unless() default "";
boolean sync() default false;
long ttl();
}
这里使用@Inherited特性,使spring能解析到我们的CacheableTtl注解。
2. TtlRedisCacheManager
TtlRedisCacheManager extends RedisCacheManager
private final RedisCacheConfiguration defaultCacheConfiguration;
public TtlRedisCacheManager(RedisCacheWriter cacheWriter,
RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
Objects.requireNonNull(defaultCacheConfiguration);
this.defaultCacheConfiguration = defaultCacheConfiguration;
}
public TtlRedisCacheManager(RedisCacheWriter cacheWriter,
RedisCacheConfiguration defaultCacheConfiguration,
String... initialCacheNames) {
super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
Objects.requireNonNull(defaultCacheConfiguration);
this.defaultCacheConfiguration = defaultCacheConfiguration;
}
public TtlRedisCacheManager(RedisCacheWriter cacheWriter,
RedisCacheConfiguration defaultCacheConfiguration,
boolean allowInFlightCacheCreation, String... initialCacheNames) {
super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
Objects.requireNonNull(defaultCacheConfiguration);
this.defaultCacheConfiguration = defaultCacheConfiguration;
}
public TtlRedisCacheManager(RedisCacheWriter cacheWriter,
RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
Objects.requireNonNull(defaultCacheConfiguration);
this.defaultCacheConfiguration = defaultCacheConfiguration;
}
public TtlRedisCacheManager(RedisCacheWriter cacheWriter,
RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations,
boolean allowInFlightCacheCreation) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
Objects.requireNonNull(defaultCacheConfiguration);
this.defaultCacheConfiguration = defaultCacheConfiguration;
}
/**
* 这个方法尤为重要,加入自定义RedisCacheManager的原因就是RedisCacheManager
* 的createCache为protected的,导致自定义的CacheResolver无法调用,因此重写此方法,
* 使其在CacheResolver可以被调用
*
* @param cacheName
* @param config
* @return
*/
public RedisCache createCache(String cacheName, RedisCacheConfiguration config) {
return super.createRedisCache(cacheName, config);
}
public RedisCacheConfiguration getCacheConfigurationCopy() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(defaultCacheConfiguration.getTtl())
.serializeKeysWith(defaultCacheConfiguration.getKeySerializationPair())
.serializeValuesWith(defaultCacheConfiguration.getValueSerializationPair())
.withConversionService(defaultCacheConfiguration.getConversionService())
.computePrefixWith(cacheName -> defaultCacheConfiguration.getKeyPrefixFor(cacheName));
}
3. TtlCacheResolver
TtlCacheResolver extends SimpleCacheResolver
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Collection<String> cacheNames = this.getCacheNames(context);
if (cacheNames == null) {
return Collections.emptyList();
} else {
Collection<Cache> result = new ArrayList(cacheNames.size());
Iterator iterator = cacheNames.iterator();
while (iterator.hasNext()) {
String cacheName = (String) iterator.next();
Cache cache = getCacheFromMem(context, cacheName);
if (cache == null) {
cache = this.getCacheManager().getCache(cacheName);
}
if (cache == null) {
throw new IllegalArgumentException(
"Cannot find cache named '" + cacheName + "' for " + context.getOperation());
}
result.add(cache);
}
return result;
}
}
private Cache getCacheFromMem(CacheOperationInvocationContext<?> context, String cacheName) {
Method method = context.getMethod();
CacheableTtl cacheableTtl = method.getAnnotation(CacheableTtl.class);
if (cacheableTtl != null && cacheableTtl.ttl() > 0) {
long ttl = cacheableTtl.ttl();
String cacheKey = cacheName + "-" + ttl;
RedisCache cache = cacheMap.get(cacheKey);
this.clearIfOverload();
if (cache == null) {
cache = this.createRedisCache(cacheName, ttl);
if (cache != null) {
cacheMap.putIfAbsent(cacheKey, cache);
}
}
return cache;
}
return null;
}
private RedisCache createRedisCache(String cacheName, Long ttl) {
CacheManager cacheManager = super.getCacheManager();
if (cacheManager instanceof TtlRedisCacheManager) {
TtlRedisCacheManager manager = (TtlRedisCacheManager) cacheManager;
RedisCacheConfiguration configuration = manager.getCacheConfigurationCopy();
return manager.createCache(cacheName, configuration.entryTtl(Duration.ofSeconds(ttl)));
}
return null;
}
private void clearIfOverload() {
if (cacheMap.size() > MAX_CACHE_SIZE) {
cacheMap.clear();
}
}
getCacheFromMem主要做的事情就是:通过反射获取ttl, 并动态生成一个RedisCacheConfiguration注入到RedisCache实例当中,这样RedisCache实例就是我们声明式注解配置的ttl时间了。
3. 配合Spring Boot
CacheableTtlAutoConfiguration
我们可以封装成一个spring boot starter jar包,实现自动化配置
@Configuration
public class CacheableTtlAutoConfiguration {
@Bean("redisSerializer")
@ConditionalOnMissingBean
public RedisSerializer redisSerializer() {
return new GenericFastJsonRedisSerializer();
}
@Bean("defaultRedisCacheConfiguration")
@ConditionalOnMissingBean
public RedisCacheConfiguration defaultRedisCacheConfiguration(RedisSerializer redisSerializer) {
return RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));
}
@Bean
@ConditionalOnMissingBean
public RedisCacheWriter redisCacheWriter(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
}
@Bean("redisCacheConfigurationMap")
@ConditionalOnMissingBean
public Map<String, RedisCacheConfiguration> redisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
return redisCacheConfigurationMap;
}
@Bean("ttlRedisCacheManager")
@ConditionalOnMissingBean
public TtlRedisCacheManager ttlRedisCacheManager(RedisCacheWriter redisCacheWriter,
RedisCacheConfiguration defaultRedisCacheConfiguration,
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap) {
return new TtlRedisCacheManager(
redisCacheWriter,
defaultRedisCacheConfiguration,
redisCacheConfigurationMap);
}
@Bean("ttlCacheResolver")
@ConditionalOnMissingBean
public TtlCacheResolver ttlCacheResolver(TtlRedisCacheManager ttlRedisCacheManager) {
return new TtlCacheResolver(ttlRedisCacheManager);
}
}