拓展SpringCache的功能(动态TTL+正则Evict)
ShiJh Lv3

SpringCache的缓存框架存在一些功能上不完善的地方,本文记录通过重写CacheManager与Cache对功能进行增强。

SpringCache拓展TTL、Evict

拓展一个框架的的功能 首先需要找到框架的拓展点,一个成熟的框架总会留下几个接口能够方便开发人员拓展自定义的功能,SpringCache也不例外。

寻找拓展点

寻找拓展点的方法如下:

  1. 阅读源码
  2. 阅读官方文档
  3. Debug调试
  4. 参考网络实践者的经验

通过以上四种方法 我把拓展点定位到了 RedisCacheManagerRedisCache

自定义RedisCacheManager实现动态TTL

SpringCache通过CacheManager接口的getCache方法获取将要缓存到Redis中的对象。在AbstractCacheManager中实现了该方法

public Cache getCache(String name) {
    Cache cache = (Cache)this.cacheMap.get(name);
    if (cache != null) {
        return cache;
    } else {
        Cache missingCache = this.getMissingCache(name);
        if (missingCache != null) {
            synchronized(this.cacheMap) {
                cache = (Cache)this.cacheMap.get(name);
                if (cache == null) {
                    cache = this.decorateCache(missingCache);
                    this.cacheMap.put(name, cache);
                    this.updateCacheNames(name);
                }
            }
        }

        return cache;
    }
}

RedisCacheManager则是通过重写 getMissingCache 来实现RedisCache的拓展

protected RedisCache getMissingCache(String name) {
    return this.allowInFlightCacheCreation ? this.createRedisCache(name, this.defaultCacheConfig) : null;
}

其中 allowInFlightCacheCreation 在默认情况下为真,因此最重要的方法便来到了 createRedisCache

protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
	return new RedisCache(name, this.cacheWriter, cacheConfig != null ? cacheConfig : this.defaultCacheConfig);
}

可以发现RedisCache使用了RedisCacheConfiguration来作为缓存的参数载体,在最终保存时用到了配置对象中的TTL来作为失效时间。

通过继承 RedisCacheManager 来进行拓展

public class RedisAutoCacheManager extends RedisCacheManager {
    //自定义缓存时间填写格式 通过 cacheName + #+时间 设定
    private static final String SPLIT_FLAG = "#";
    //校验值常量
    private static final int CACHE_LENGTH = 2;
    //自定义缓存时间单位
    private static final String MINUS_UNIT = "m";
    private static final String HOUR_UNIT = "h";
    private static final String SECOND_UNIT = "s";
    //以下两对象在父类中为private 因此需要记得在继承的构造方法中另外获取
    private final RedisCacheWriter writer;
    private final RedisCacheConfiguration defaultRedisConfig;

    //constructor 省略构造函数

    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        //未设置则走默认方法
        if (name.isBlank() || !name.contains(SPLIT_FLAG)) {
            return getAutoRedisCache(name, cacheConfig);
        }

        String[] cacheArray = name.split(SPLIT_FLAG);
        //真实的cacheName
        String curName = cacheArray[0];
        //#后未填写时间走默认方法
        if (cacheArray.length < CACHE_LENGTH) {
            return getAutoRedisCache(curName, cacheConfig);
        }

        if (cacheConfig != null) {
            String cacheTTL = cacheArray[1].toLowerCase();
            Duration duration;
            //获取缓存TTL
            if (cacheTTL.contains(MINUS_UNIT)) {
                duration = Duration.ofMinutes(getTTL(cacheTTL, MINUS_UNIT, 1));
            } else if (cacheTTL.contains(HOUR_UNIT)) {
                duration = Duration.ofHours(getTTL(cacheTTL, HOUR_UNIT, 1));
            } else if (cacheTTL.contains(SECOND_UNIT)) {
                duration = Duration.ofSeconds(getTTL(cacheTTL, SECOND_UNIT, 1));
            } else {
                return getAutoRedisCache(curName, cacheConfig);
            }
            log.info("generate cache with ttl {}-{}", cacheTTL, duration);
            //自定义缓存TTL
            cacheConfig = cacheConfig.entryTtl(duration);
        }
        return getAutoRedisCache(curName, cacheConfig);
    }

    private RedisCache getAutoRedisCache(String name, RedisCacheConfiguration cacheConfig) {]
        //RedisAutoCache extends RedisCache
        //如果没有cacheConfig则使用默认的redisCacheConfig
        return new RedisAutoCache(name, this.writer, cacheConfig != null ? 
                                  cacheConfig : this.defaultRedisConfig);
    }
	
    private long getTTL(String cacheTTL, String unit, long def) {
        String[] split = cacheTTL.split(unit);
        if (split.length > CACHE_LENGTH) {
            return Long.parseLong(split[0]);
        }
        return def;
    }
}

自定义Cache实现正则删除

Cacheevict 用于执行缓存失效的方法,RedisCacheWriter 为具体执行单位,其中的 clear 方法即通过 keys pattern 获取符合表达式的 key 再执行批量方法

public class RedisAutoCache extends RedisCache {
    private final RedisCacheWriter cacheWriter;
    protected RedisAutoCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
        super(name, cacheWriter, cacheConfig);
        this.cacheWriter = cacheWriter;
    }


    /**
     * 提供批量删除
     * @param key 带 ‘*’ 的key
     */
    @Override
    public void evict(Object key) {
        String cacheKey = this.createCacheKey(key);
		//如果包含通配符则使用批量删除
        if (cacheKey.contains("*")) {
            log.info("cache multi evict, pattern {}",cacheKey);
            cacheWriter.clean(this.getName(), this.serializeCacheKey(cacheKey));
        } else {
            super.evict(key);
        }
    }
}

自定义配置文件

SpringCache对应的自动配置文件在获取不到 CacheManager对象时才会执行,自定义RedisCacheManager 后 需要自行编写配置文件:

@Configuration(proxyBeanMethods = false)
//获取application.yml中填写的配置信息 该对象仅在自动装配时注入 自定义时需自行引入
@EnableConfigurationProperties({CacheProperties.class})
public class RedisConfig {
    
    //注入自定义CacheManager
    @Bean
    public CacheManager redisAutoCacheManager(RedisCacheWriter writer, RedisCacheConfiguration defConfig) {
        return new RedisAutoCacheManager(writer,defConfig);
    }
    
	//从静态方法中获取RedisCacheWriter 
    //nonlocking表示无锁,缓存操作将同时进行,性能较高但可能返回过时数据
    //有提供 lockingRedisCacheWriter
    @Bean
    @ConditionalOnMissingBean
    public RedisCacheWriter redisCacheWriter(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
    }

    /**
     * 注入自定义默认配置文件 spring将会使用此Bean作为RedisCacheManager配置文件
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties,
                                                           RedisTemplate<Object,Object> redisTemplate) {
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        RedisCacheConfiguration config = RedisCacheConfiguration
                .defaultCacheConfig().serializeValuesWith(
                        RedisSerializationContext
                                .SerializationPair.fromSerializer(redisTemplate.getValueSerializer())
                );

        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }

        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }

        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;
    }
}

可能存在的问题

CacheAutoConfiguration 中并没有显式的注入CacheManager 而是提供了其他一系列对象,暂时不清楚其用途,自定义配置文件中未注入这些对象可能会引发一些问题,我在实际使用中暂时还未发现,目前仍在正常使用。

 评论