原创

Spring Cache 与 Redis:缓存注解、配置和一致性边界

整理 Spring Cache 的缓存抽象与 Redis 配置,说明缓存键、更新失效和代理调用的检查方法,帮助把注解使用与业务一致性联系起来。

缓存键应表达完整查询条件,否则不同业务请求可能共享同一结果。更新数据时要同时考虑数据库事务、缓存失效时机和失败恢复。

官网地址:https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache

一 理解缓存抽象

1. 官网文档

与 Spring Framework 中的其他服务一样,缓存服务是一种抽象(不是缓存实现),需要使用实际存储来存储缓存数据——也就是说,抽象使您不必编写缓存逻辑,但确实不提供实际的数据存储。这种抽象是由 org.springframework.cache.Cache和org.springframework.cache.CacheManager接口实现的。

2. 注意

要使用缓存抽象,您需要注意两个方面:

  • 缓存声明:标识需要缓存的方法及其策略。
  • 缓存配置:存储数据和从中读取数据的后备缓存。

二 使用SpringCache

1. 加入依赖

  <!-- SpringCache依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

2. 在启动类上开启缓存

@EnableCaching

3. 在需要使用注解的方法上添加对应注解

@Cacheable: Triggers cache population.   添加缓存

@CacheEvict: Triggers cache eviction.  移除缓存

@CachePut: Updates the cache without interfering with the method execution.  在不干扰方法执行的情况下更新缓存

@Caching: Regroups multiple cache operations to be applied on a method.  重新组合多个缓存操作以应用于一个方法

@CacheConfig: Shares some common cache-related settings at class-level.  在类级别共享一些常见的缓存相关设置。

3. 自定义缓存配置

  1. 找到cache的自动化配置类 CacheAutoConfiguration

  2. 该类会自动导入配置对应相关的组件

    	static class CacheConfigurationImportSelector implements ImportSelector {
    
    		@Override
    		public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    			CacheType[] types = CacheType.values();
    			String[] imports = new String[types.length];
    			for (int i = 0; i < types.length; i++) {
    				imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
    			}
    			return imports;
    		}
    
    	}
    
    

    CacheConfigurations类包含的配置

    static {
    		Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);
    		mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
    		mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
    		mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
    		mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
    		mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
    		mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
    		mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
    		mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
    		mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
    		mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
    		MAPPINGS = Collections.unmodifiableMap(mappings);
    	}
    

    我们在配置文件中使用的是redis,所以此处会自动导入redis的自动配置类RedisCacheConfiguration

  3. RedisCacheConfiguration配置类中自动配置了redis的缓存管理器

    	@Bean
    	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));
    		}
    		redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
    		return cacheManagerCustomizers.customize(builder.build());
    	}
    
    1. redisCache配置都在对应的org.springframework.data.redis.cache.RedisCacheConfiguration配置类中

      	//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;
      
      	@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;
      	}
      
      1. 配置源码分析

        /*
         * Copyright 2012-2019 the original author or authors.
         *
         * Licensed under the Apache License, Version 2.0 (the "License");
         * you may not use this file except in compliance with the License.
         * You may obtain a copy of the License at
         *
         *      https://www.apache.org/licenses/LICENSE-2.0
         *
         * Unless required by applicable law or agreed to in writing, software
         * distributed under the License is distributed on an "AS IS" BASIS,
         * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         * See the License for the specific language governing permissions and
         * limitations under the License.
         */
        
        package org.springframework.boot.autoconfigure.cache;
        
        import java.util.LinkedHashSet;
        import java.util.List;
        
        import org.springframework.beans.factory.ObjectProvider;
        import org.springframework.boot.autoconfigure.AutoConfigureAfter;
        import org.springframework.boot.autoconfigure.cache.CacheProperties.Redis;
        import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
        import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
        import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
        import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
        import org.springframework.cache.CacheManager;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Conditional;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.core.io.ResourceLoader;
        import org.springframework.data.redis.cache.RedisCacheManager;
        import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder;
        import org.springframework.data.redis.connection.RedisConnectionFactory;
        import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
        import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
        
        /**
         * Redis cache configuration.
         *
         * @author Stephane Nicoll
         * @author Mark Paluch
         * @author Ryon Day
         */
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass(RedisConnectionFactory.class)
        @AutoConfigureAfter(RedisAutoConfiguration.class)
        @ConditionalOnBean(RedisConnectionFactory.class)
        @ConditionalOnMissingBean(CacheManager.class)
        @Conditional(CacheCondition.class)
        class RedisCacheConfiguration {
        
        	@Bean
        	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));
        		}
        		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) {
        		//	1.调用createConfiguration方法,返回默认的缓存管理器
        		//  2. redisCacheConfiguration.getIfAvailable...  见下
        		return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
        	}
        
        	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.prefixKeysWith(redisProperties.getKeyPrefix());
        		}
        		if (!redisProperties.isCacheNullValues()) {
        			config = config.disableCachingNullValues();
        		}
        		if (!redisProperties.isUseKeyPrefix()) {
        			config = config.disableKeyPrefix();
        		}
        		return config;
        	}
        
        }
        
        

        5.1 redisCacheConfiguration.getIfAvailable方法实现类ClientFactoryObjectProvider

        	@Override
        	public T getIfAvailable(Supplier<T> defaultSupplier) throws BeansException {
        		return delegate().getIfAvailable(defaultSupplier);
        	}
        
        		private ObjectProvider<T> delegate() {
        		if (this.provider == null) {
        			this.provider = this.clientFactory.getProvider(this.name, this.type);
        		}
        		return this.provider;
        	}
        

        delegate().getIfAvailable(defaultSupplier) 在容器中通过对象查询bean。如果有,则返回容器中的对象,如果没有就返回当前对象。(通过生成的默认缓存配置去容器中查找是否有相同的Bean(是否已经存在),如果有则返回对应的Bean,如果没有则返回当前配置类)

        1. 对此,我们只需要在配置文件中新增对应的Bean RedisCacheConfiguration即可

          import org.springframework.boot.autoconfigure.cache.CacheProperties;
          import org.springframework.boot.context.properties.EnableConfigurationProperties;
          import org.springframework.cache.annotation.EnableCaching;
          import org.springframework.context.annotation.Bean;
          import org.springframework.context.annotation.Configuration;
          import org.springframework.data.redis.cache.RedisCacheConfiguration;
          import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
          import org.springframework.data.redis.serializer.RedisSerializationContext;
          import org.springframework.data.redis.serializer.StringRedisSerializer;
          
          
          @Configuration
          @EnableCaching
          @EnableConfigurationProperties(CacheProperties.class)//使CacheProperties配置文件生效,可以在容器中直接使用
          public class MyRedisConfigure  {
          
              //1. CacheProperties可以使用自动注解使用
          //    @Autowired
          //    CacheProperties cacheProperties;
          
              /**
               * remark:@ConfigurationProperties(prefix = "spring.cache")
               * public class CacheProperties
               *  配置文件中的cache配置在这个类生效,使用自定义配置后会失效
                  2.CacheProperties直接在方法上使用
               */
              @Bean
              RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
          
                  //配置key和val的序列化方式
                  RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
                  config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class)));
                  config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
          
                  //获取cache的配置文件配置,使其生效
                  CacheProperties.Redis redisProperties = cacheProperties.getRedis();
                  if (redisProperties.getTimeToLive() != null) {
                      config = config.entryTtl(redisProperties.getTimeToLive());
                  }
                  if (redisProperties.getKeyPrefix() != null) {
                      config = config.prefixKeysWith(redisProperties.getKeyPrefix());
                  }
                  if (!redisProperties.isCacheNullValues()) {
                      config = config.disableCachingNullValues();
                  }
                  if (!redisProperties.isUseKeyPrefix()) {
                      config = config.disableKeyPrefix();
                  }
                  return config;
              }
          
          
          }
          

4. 缓存注解使用方式

https://docs.spring.io/spring-framework/docs/5.2.19.RELEASE/spring-framework-reference/integration.html#cache

     执行时清除category组下 key为getOneLevelCategory的缓存
     @CacheEvict(value = {"category"},key = "'getOneLevelCategory'")
	 执行时会清除所有category组下的缓存
	 @CacheEvict(value = {"category"},allEntries = true)
	 先清除category组下 key为getOneLevelCategory的缓存 再向category组下缓存key为temp的值
     @Caching(cacheable = {@Cacheable(value = {"category"},key = "'temp'")},
		evict = { @CacheEvict(value = {"category"},key = "'getOneLevelCategory'")}
		)
	先清除category组下 key为getOneLevelCategory的缓存。再缓存
    @CachePut(value = {"category"},key = "'getOneLevelCategory'")
正文到此结束