Spring基于redis动态缓存过期时间

311 阅读5分钟

目前公司业务需要根据当前时间,动态缓存时间,缓存失效时间是根据业务动态计算出来的。

解决方法

  1. 自定义aop缓存
  2. 修改spring自带的@EnableCaching缓存

我选择了方法2,毕竟我也是熟读源码的人,这一点点问题还是要挑战一下的,完全自定义了还有啥意思。

Spring缓存源码加载流程

  1. 缓存入口类

    @EnableCaching
    
  2. @Import自动导入

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(CachingConfigurationSelector.class)
    public @interface EnableCaching {
    
    	/**
    	 * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
    	 * to standard Java interface-based proxies. The default is {@code false}. <strong>
    	 * Applicable only if {@link #mode()} is set to {@link AdviceMode#PROXY}</strong>.
    	 * <p>Note that setting this attribute to {@code true} will affect <em>all</em>
    	 * Spring-managed beans requiring proxying, not just those marked with {@code @Cacheable}.
    	 * For example, other beans marked with Spring's {@code @Transactional} annotation will
    	 * be upgraded to subclass proxying at the same time. This approach has no negative
    	 * impact in practice unless one is explicitly expecting one type of proxy vs another,
    	 * e.g. in tests.
    	 */
    	boolean proxyTargetClass() default false;
    
    	/**
    	 * Indicate how caching advice should be applied.
    	 * <p><b>The default is {@link AdviceMode#PROXY}.</b>
    	 * Please note that proxy mode allows for interception of calls through the proxy
    	 * only. Local calls within the same class cannot get intercepted that way;
    	 * a caching annotation on such a method within a local call will be ignored
    	 * since Spring's interceptor does not even kick in for such a runtime scenario.
    	 * For a more advanced mode of interception, consider switching this to
    	 * {@link AdviceMode#ASPECTJ}.
    	 */
    	AdviceMode mode() default AdviceMode.PROXY;
    
    	/**
    	 * Indicate the ordering of the execution of the caching advisor
    	 * when multiple advices are applied at a specific joinpoint.
    	 * <p>The default is {@link Ordered#LOWEST_PRECEDENCE}.
    	 */
    	int order() default Ordered.LOWEST_PRECEDENCE;
    
    }
    
  3. Import导入类CachingConfigurationSelector,是实现ImportSelector接口的,那我们看selectImports方法

    	/**
    	 * Returns {@link ProxyCachingConfiguration} or {@code AspectJCachingConfiguration}
    	 * for {@code PROXY} and {@code ASPECTJ} values of {@link EnableCaching#mode()},
    	 * respectively. Potentially includes corresponding JCache configuration as well.
    	 */
    	@Override
    	public String[] selectImports(AdviceMode adviceMode) {
    		switch (adviceMode) {
    			case PROXY:
    				return getProxyImports(); // 走这个,看注解默认值
    			case ASPECTJ:
    				return getAspectJImports();
    			default:
    				return null;
    		}
    	}
    
    	/**
    	 * Return the imports to use if the {@link AdviceMode} is set to {@link AdviceMode#PROXY}.
    	 * <p>Take care of adding the necessary JSR-107 import if it is available.
    	 */
    	private String[] getProxyImports() {
    		List<String> result = new ArrayList<>(3);
    		result.add(AutoProxyRegistrar.class.getName());
    		result.add(ProxyCachingConfiguration.class.getName());
    		if (jsr107Present && jcacheImplPresent) {
    			result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
    		}
    		return StringUtils.toStringArray(result);
    	}
    
  4. 看ProxyCachingConfiguration实现,肯定是生成缓存代理的类,重要的三元素(Advisor、Pointcut、Advice),具体逻辑在Advice里面,我们只要看Advice的invoke方法即可。

    @Configuration(proxyBeanMethods = false)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
    
    	@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
    	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    	public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(
    			CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
    
    		BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
    		advisor.setCacheOperationSource(cacheOperationSource);
    		advisor.setAdvice(cacheInterceptor); // 这里说明cacheInterceptor是advice
    		if (this.enableCaching != null) {
    			advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
    		}
    		return advisor;
    	}
    
    	@Bean
    	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    	public CacheOperationSource cacheOperationSource() {
    		return new AnnotationCacheOperationSource();
    	}
    
    	@Bean
    	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    	public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) {
    		CacheInterceptor interceptor = new CacheInterceptor(); //这是是创建advice
    		interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
    		interceptor.setCacheOperationSource(cacheOperationSource);
    		return interceptor;
    	}
    
    }
    
  5. 我们直接看CacheInterceptor的invoke方法即可

    public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
    
       @Override
       @Nullable
       public Object invoke(final MethodInvocation invocation) throws Throwable {
          Method method = invocation.getMethod();
    
          CacheOperationInvoker aopAllianceInvoker = () -> {
             try {
                return invocation.proceed();
             }
             catch (Throwable ex) {
                throw new CacheOperationInvoker.ThrowableWrapper(ex);
             }
          };
    
          Object target = invocation.getThis();
          Assert.state(target != null, "Target must not be null");
          try {
             return execute(aopAllianceInvoker, target, method, invocation.getArguments());// 核心方法
          }
          catch (CacheOperationInvoker.ThrowableWrapper th) {
             throw th.getOriginal();
          }
       }
    
    }
    
  6. 我们看execute核心方法

    	@Nullable
    	protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
    		// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
    		if (this.initialized) {
    			Class<?> targetClass = getTargetClass(target);
    			CacheOperationSource cacheOperationSource = getCacheOperationSource();
    			if (cacheOperationSource != null) {
            // 获取方法缓存注解等信息包装类
    				Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
    				if (!CollectionUtils.isEmpty(operations)) {
    					return execute(invoker, method,
    							new CacheOperationContexts(operations, method, args, target, targetClass));// 核心方法
    				}
    			}
    		}
    
    		return invoker.invoke();
    	}
    
  7. 最最最核心方法

    @Nullable
    	private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
    		// Special handling of synchronized invocation
    		if (contexts.isSynchronized()) {
    			CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
    			if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
    				Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
    				Cache cache = context.getCaches().iterator().next();
    				try {
    					return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
    				}
    				catch (Cache.ValueRetrievalException ex) {
    					// Directly propagate ThrowableWrapper from the invoker,
    					// or potentially also an IllegalArgumentException etc.
    					ReflectionUtils.rethrowRuntimeException(ex.getCause());
    				}
    			}
    			else {
    				// No caching required, only call the underlying method
    				return invokeOperation(invoker);
    			}
    		}
    
    
    		//先处理@CacheEvicts的逻辑,,其实就是掉clear方法
    		// Process any early evictions
    		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
    				CacheOperationExpressionEvaluator.NO_RESULT);
    
    		//处理@Cacheable的逻辑,,其实就是掉get方法
    		// Check if we have a cached item matching the conditions
    		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
    
    		// Collect puts from any @Cacheable miss, if no cached item is found
    		List<CachePutRequest> cachePutRequests = new LinkedList<>();
    		//如果缓存没命中或者不是使用的@Cacheable注解
    		if (cacheHit == null) {
    			//处理@Cacheable的逻辑,收集插入请求,插入缓存的值需要调用被代理方法
    			collectPutRequests(contexts.get(CacheableOperation.class),
    					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
    		}
    
    		Object cacheValue;
    		Object returnValue;
    
    		//如果缓存命中了
    		if (cacheHit != null && !hasCachePut(contexts)) {
    			// If there are no put requests, just use the cache hit
    			cacheValue = cacheHit.get();
    			//直接返回缓存中的值
    			returnValue = wrapCacheValue(method, cacheValue);
    		}
    		else {
    			//在这里调用被代理方法
    			// Invoke the method if we don't have a cache hit
    			returnValue = invokeOperation(invoker);
    			cacheValue = unwrapReturnValue(returnValue);
    		}
    
    		//处理@CachePut注解,收集put请求
    		// Collect any explicit @CachePuts
    		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
    
    		//处理put请求,其实就是掉put方法
    		// Process any collected put requests, either from @CachePut or a @Cacheable miss
    		for (CachePutRequest cachePutRequest : cachePutRequests) {
    			cachePutRequest.apply(cacheValue); // 主要看这个方法
    		}
    
    		// Process any late evictions
    		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
    
    		return returnValue;
    	}
    
  8. 通过上面代码,我们主要集中看apply方法,因为这边是设置缓存

    		public void apply(@Nullable Object result) {
    			if (this.context.canPutToCache(result)) {
    				for (Cache cache : this.context.getCaches()) {
    					doPut(cache, this.key, result); // 核心方法
    				}
    			}
    		}
    
    
    
    	/**
    	 * Execute {@link Cache#put(Object, Object)} on the specified {@link Cache}
    	 * and invoke the error handler if an exception occurs.
    	 */
    	protected void doPut(Cache cache, Object key, @Nullable Object result) {
    		try {
    			cache.put(key, result); // 核心方法
    		}
    		catch (RuntimeException ex) {
    			getErrorHandler().handleCachePutError(ex, cache, key, result);
    		}
    	}
    
  9. 这边我们主要看一下cache对象基于RedisCache的实现,毕竟我们用的是redis的缓存。下面方法中的getTtl就是我们的突破口,如果我们能将getTtl方法返回一个动态的值,那我们的业务不就实现了吗???

    哇喔,想想就激动!!!

    @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()); // 获取过期时间
    }
    
  10. 那问题来了,这个过期时间怎么来的?我们只要分析一下RedisCacheConfiguration这个对象中的ttl属性怎么来的即可。

    public class RedisCacheConfiguration {
    
    	private final Duration ttl;
    	private final boolean cacheNullValues;
    	private final CacheKeyPrefix keyPrefix;
    	private final boolean usePrefix;
    
    	private final SerializationPair<String> keySerializationPair;
    	private final SerializationPair<Object> valueSerializationPair;
    
    	private final ConversionService conversionService;
    
      // 有且仅有一个构造方法,还是私有的,那肯定有static方法
    	@SuppressWarnings("unchecked")
    	private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix,
    			SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
    			ConversionService conversionService) {
    
    		this.ttl = ttl;
    		this.cacheNullValues = cacheNullValues;
    		this.usePrefix = usePrefix;
    		this.keyPrefix = keyPrefix;
    		this.keySerializationPair = keySerializationPair;
    		this.valueSerializationPair = (SerializationPair<Object>) valueSerializationPair;
    		this.conversionService = conversionService;
    	}
    }
    
  11. 我们看下这个类结构,有大量的static,并且返回值是本实例对象,那不久就是类似于builder的建造器么?

    截屏2023-03-22 下午5.54.04

  12. 那我们接下来肯定要搞明白哪里调用初始化了RedisCacheConfiguration对象,其实想也不用想,肯定是利用的springboot的spi机制,而且一般就在sprin-boot-autoconfigure.jar里面。

    我们在RedisCacheConfiguration的defaultCacheConfig方法上点一下,果然找到了RedisCacheConfiguration类。

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RedisConnectionFactory.class)
    @AutoConfigureAfter(RedisAutoConfiguration.class)
    @ConditionalOnBean(RedisConnectionFactory.class)
    @ConditionalOnMissingBean(CacheManager.class)// 如果spring容器中有一个CacheManager对象,这个类就不加载
    @Conditional(CacheCondition.class)
    class RedisCacheConfiguration {
    
      // 定义了RedisCacheManager对象
       @Bean
      // cacheProperties 配置缓存属性
       RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
             ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
             ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
             RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
          RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
                determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
          List<String> cacheNames = cacheProperties.getCacheNames();
          if (!cacheNames.isEmpty()) {
             builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
          }
          if (cacheProperties.getRedis().isEnableStatistics()) {
             builder.enableStatistics();
          }
          redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
          return cacheManagerCustomizers.customize(builder.build());
       }
    
       private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
             CacheProperties cacheProperties,
             ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
             ClassLoader classLoader) {
          return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
       }
    
      // 我们的RedisCacheConfiguration对象就在这。
       private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
             CacheProperties cacheProperties, ClassLoader classLoader) {
          Redis redisProperties = cacheProperties.getRedis();
          org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
                .defaultCacheConfig();
          config = config.serializeValuesWith(
                SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
          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;
       }
    
    }
    
  13. 我们在步骤9看到,RedisCache对象是持有RedisCacheConfiguration对象的,那什么时候放进去的呢?想必一定和RedisCacheManager有关,因为我们RedisCacheConfiguration是赋值给了RedisCacheManager。

    RedisCacheManager类的代码,我们可以看出在createRedisCache方法,源码将defaultCacheConfig放进了RedisCache对象

    /**
     * Configuration hook for creating {@link RedisCache} with given name and {@code cacheConfig}.
     *
     * @param name must not be {@literal null}.
     * @param cacheConfig can be {@literal null}.
     * @return never {@literal null}.
     */
    protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
       return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
    }
    

修改思路

  1. 首先我们要自定义类,继承RedisCache,并重写put方法,将ttl设置成我们的业务逻辑(参考步骤9)
  2. 要自定义RedisCache,也必须自定义一个类继承RedisCacheManager,修改RedisCacheManager的createRedisCache方法,返回自定义的RedisCache对象。(参考步骤9)
  3. 最后我们必须自定义RedisCacheManager,覆盖springboot中默认的RedisCacheManager。

实现代码

  1. 自定义RedisCache

    public class CGRedisCache extends RedisCache {
        /**
         * Create new {@link RedisCache}.
         *
         * @param redisCache        must not be {@literal null}.
         */
        protected CGRedisCache(RedisCache redisCache) {
            super(redisCache.getName(), redisCache.getNativeCache(), redisCache.getCacheConfiguration());
        }
    
    
        @Override
        public void put(Object key, 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.",
                        getName()));
            }
    
            getNativeCache().put(getName(), serializeCacheKey(createCacheKey(key)), serializeCacheValue(cacheValue), getTtl());
        }
    
        private Duration getTtl() {
            // 实现自己的业务逻辑吧
            return null;
        }
    
        @Override
        public ValueWrapper putIfAbsent(Object key, Object value) {
            Object cacheValue = preProcessCacheValue(value);
    
            if (!isAllowNullValues() && cacheValue == null) {
                return get(key);
            }
    
            byte[] result = getNativeCache().putIfAbsent(getName(), serializeCacheKey(createCacheKey(key)), serializeCacheValue(cacheValue),
                    getTtl());
    
            if (result == null) {
                return null;
            }
    
            return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
        }
    }
    
  2. 自定义RedisCacheManager

    public class CGRedisCacheManager extends RedisCacheManager {
    
        public CGRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
            super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
        }
    
        @Override
        protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
            RedisCache redisCache = super.createRedisCache(name, cacheConfig);
            return new CGRedisCache(redisCache);
        }
    }
    
  3. 讲自定义的RedisCacheManager加入到spring容器中,覆盖默认配置

    @Configuration
    @EnableCaching
    public class RedisCacheConfig {
    
        /**
         * 自定义缓存管理器
         */
        @Bean
        public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
    
            Jackson2JsonRedisSerializer valueRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    
    
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
            valueRedisSerializer.setObjectMapper(objectMapper);
    
            RedisSerializationContext.SerializationPair<Object> v = RedisSerializationContext.SerializationPair.fromSerializer(valueRedisSerializer);
    
            CacheKeyPrefix cacheKeyPrefix = cacheName -> "Children:api:cache:";
    
    
            RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(1L))
                    .computePrefixWith(cacheKeyPrefix)
                    .serializeValuesWith(v)
                    .disableCachingNullValues();
    
            // 针对不同cacheName,设置不同的过期时间
            Map<String, RedisCacheConfiguration> initialCacheConfiguration = new HashMap<String, RedisCacheConfiguration>() {{
                put("ttl1", RedisCacheConfiguration.defaultCacheConfig().computePrefixWith(cacheKeyPrefix).serializeValuesWith(v).entryTtl(Duration.ofSeconds(1L)));
                put("ttl3", RedisCacheConfiguration.defaultCacheConfig().computePrefixWith(cacheKeyPrefix).serializeValuesWith(v).entryTtl(Duration.ofSeconds(3L)));
            }};
    
            return new CGRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(factory), defaultCacheConfig, initialCacheConfiguration);
        }
    
    }
    

总结

读懂源码很重要,整体修改还是比较简单的,就是说一下spring-boot源码写的有一点点坑,各种private,不过也还好。