Spring Cacheable(Redis)扩展实现注解式TTL

一、背景

我们在项目当中经常要使用redis作为缓存,可选择的方式有:1)编程方式 2)注解方式

1. 编程方式

  1. jedis
  2. redisTempate
  3. redisson

2. 注解方式

  1. spring cache系列注解
  2. 通过切面自定义注解实现
    由于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);
    }
}

附件

Gitee:https://gitee.com/liuyangrxy/carp.git

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值