SpringBoot 2.x RedisCache 实现自定义过期时间(注解式)

背景

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 的方法的;

源码分析

  1. 获取请求,判断当前请求请求的方法是否有@Cacheable 注解,如果有会触发org.springframework.cache.interceptor.CacheInterceptor 拦截器;
  2. 调用CacheManager 的getCache方法,如果有缓存则返回查询到的缓存Cache,否则新创建一个缓存并返回;源码如下(AbstractCacheManager 中):
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;
	}

  1. 获取到Cahce 后,调用get 方法获取Cache 中的缓存结果,主要在RedisCache 中进行操作,该RedisCache 中可以满足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());
	}

实现思路

  1. 通过上边源码分析,可以发现,Cache 进行操作Redis 其实是通过RedisCache 中的get ,put 等方法进行操作的,如果想通过注解式配置key 的TTL ,可以通过重写RedisCache 中的put 方法来实现,并且重写RedisCacheManager 调用我们重写后的RedisCache类;
    编码如下:
    重写RedisCache
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;
    }

你可能感兴趣的:(redis)