Springboot 整合 SpringCache

简单整理一下 SpringCache + Redis的使用,更具体的说明,可以看 Spring 官方关于SpringCache的说明

1、引入依赖

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

2、添加配置

  • 配置使用 Redis 作为缓存
spring:
  cache:
    type: redis 				  #指定使用redis作为缓存

3、测试使用SpringCache缓存

1、标注 @EnableCaching到启动类或者配置类上,开启缓存功能

2、使用 SpringCache缓存注解标注到业务方法上,常用的注解如下:

对于缓存声明,Spring的缓存抽象提供了一组Java注解:

  • @Cacheable:触发缓存填充的操作。
  • @CacheEvict: 触发缓存删除的操作。
  • @CachePut:在不干扰方法执行的情况下更新缓存。
  • @Caching:重新组合多个缓存操作以应用于一个方法,即使用该注解内部可以组合使用其他注解(@CacheEvict@CachePut@Cacheable)
  • @CacheConfig:在类级别共享一些常见的缓存相关设置。

@Cacheable的使用为例:

    //1、Cacheable代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有会调用方法,最后将方法的结果放入缓存
	//2、每一个需要缓存的数据,都要指定要放到哪个名字的缓存分区。【缓存的分区(建议按照业务类型分,方便统一删除)】
	@Cacheable(value = {"category"}) 
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        // 查db操作 ,略;
    }

测试发现, SpringCahe默认行为如下:

1)、如果缓存中有数据,该方法不用被调用;

2)、key默认自动生成: 缓存的名字::SimpLeKey[](自主生成的key值);

3)、缓存的 value 的值。默认使用 “jdk序列化机制 org.springframework.data.redis.serializer.JdkSerializationRedisSerializer”,将序列化后的数据存到 redis ;

4)、默认 ttl 时间 -1,永久;

自定义配置:

1)、指定生成的缓存使用的 key; key属性指定时,接受一个SpEL的表达式,如:@Cacheable(value = {"category"},key = "#root.methodName") ,其中 key = "#root.methodName",意思是使用方法名作为key。其他用法详见 Available Caching SpEL Evaluation Context

2)、指定缓存的数据的存活时间 ttl:配置文件中修改 ttl 即可。

spring:
  cache:
    type: redis 				  #指定使用redis作为缓存
    redis:
#      key-prefix:                # 配置所有缓存key的前缀,不配置,默认格式为  "缓存自定义的value::SimpleKey []"
      time-to-live: 3600000        # 配置 ttl 缓存过期时间,单位毫秒,这里设置了3600秒,即1个钟失效
      cache-null-values: true     # 是否缓存 Null 值,默认为 true,默认解决“缓存穿透”问题。
      use-key-prefix: true        # 是否使用key前缀,默认为 true

3)、将数据保存为json格式 :使用自定义缓存配置类,覆盖默认的配置。

import org.springframework.boot.autoconfigure.cache.CacheProperties;
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.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;

/**
 * @description 自定义缓存配置
 */
@Configuration
public class MyCacheConfig {

    /**
     * 自定义 redis 的缓存配置(重写默认配置)
     * @param cacheProperties cache的配置对象
     * @return {@link RedisCacheConfiguration}
     */
    @Bean
    public RedisCacheConfiguration createConfiguration(CacheProperties cacheProperties) {
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        //获取 默认的缓存配置,后面覆写配置。
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

        //设置 key序列器: 使用默认的 {@link StringRedisSerializer}
        config.serializeKeysWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        //设置 value序列器: 使用 {@link GenericFastJsonRedisSerializer}
        config = config.serializeValuesWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));

        //如下代码 照抄默认配置源码,不变
        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;
    }
}

RedisSerializer 有很多实现类,选择一个 JSON转换的实现类作为 redis的 value 序列化实现类 即可。

RedisSerializer 有很多实现类

@CacheEvict@Caching的使用:

  • @CacheEvict 代表当前方法如果执行,则删除指定的缓存;一般用于更新接口。

  • 查询接口搭配使用 @Cacheable注解,等触发查询接口时,重新将数据库中新数据缓存起来,@CacheEvict 注解的作用相当于缓存中的“失效模式”。

//1、指定删除“category"缓存分区里,某个缓存key的数据。
@CacheEvict(value = {"category"},key = "'getLevel1Categorys'")

//2、指定删除“category"缓存分区里,某个缓存 key1、key2...keyN 的数据 (批量删)。
@Caching(evict = {
    @CacheEvict(value = {"category"}, key = "'getLevel1Categorys'"),
    @CacheEvict(value = {"category"}, key = "'getCatelogJson'"),
    // @CacheEvict(....) 可以删除多个
	}
)

//3、指定删除“category"缓存分区所有数据。比 (2)的删除方式更方便快捷,批量删推荐使用这种方式。
@CacheEvict(value = {"category"},allEntries = true)
@Transactional
@Override
public void updateCascade(CategoryVo categoryVo) {
  //略
}

//代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有会调用方法,最后将方法的结果放入缓存
@Cacheable(value = {"category"}, key = "#root.methodName")
@Override
public List<CategoryEntity> getLevel1Categorys() {
    //略
}

@Cacheable(value = {"category"}, key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
    //略
}

4、Spring-Cache的不足

1)、读模式:

  • 缓存穿透:查询一个null数据。

    • 解决办法:默认缓存空数据,
    • Cache解决:默认配置支持缓存Null数据,配置属性为:spring.cache.redis.cache-null-values=true
  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。

    • 解决办法:加锁;
    • Cache解决?:Cache默认是无加锁的;不过缓存查询时可以使用sync = true属性加本地进程锁。sync = true意思是查询缓存时会使用“本地进程锁”,用法如: @Cacheablesyc(sync = true) ,详情见下面的“源码分析-RedisCache”。
  • 缓存雪崩:大量的key同时过期。

    • 解决办法:加随机时间。
    • Cache解决?: 默认配置为-1永久。可以修改配置加上过期时间。配置属性为:spring.cache.redis.time-to-live

2)、写模式:(缓存与数据库一致性)

  • 读写加锁

    • 读锁:Spring-Cache 可以使用 @Cacheable(sync = true),支持读的时候加本地锁。
    • 写锁:Spring-Cache 默认不支持。
  • 即时性、一致性:Spring-Cache 均不能保证即时性,数据库与缓存的一致性。

总结:

  • 常规数据(读多写少、即时性、一致性要求不高的数据):完全可以使用 Spring-Cache 来缓存支持。Spring-Cache 能设置缓存的过期时间,利用 @CachePut 注解实现“双写模式”;利用 @CacheEvict 注解实现“失效模式”,一般的常规场景也足够用了。

  • 特殊数据

    • 读多写多:直接去数据库查询就行。

    • 即时性、一致性高:引入Canal,通过日志记录文件感知到MySOL的更新后,再去更新Redis缓存库。

5、源码分析

SpringBoot 框架在引入 spring-boot-starter-cachespring-boot-starter-data-redis 依赖后,帮我们自动配置了哪些东西?

CacheAutoConfiguration

  • CacheAutoConfiguration 会自动导入 RedisCacheConfiguration ,RedisCacheConfiguration 会导入各种 xxxCacheConfiguration 缓存配置。
@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({CacheManager.class})
@ConditionalOnBean({CacheAspectSupport.class})
@ConditionalOnMissingBean(
    value = {CacheManager.class},
    name = {"cacheResolver"}
)
// 1、CacheProperties 类是 SpringCache的配置,这里导入对应的配置。
@EnableConfigurationProperties({CacheProperties.class})

@AutoConfigureAfter({CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class})
@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class, CacheAutoConfiguration.CacheManagerEntityManagerFactoryDependsOnPostProcessor.class})
public class CacheAutoConfiguration {
    //导入的组件,略,只标注重点的入口,具体可以自己看源码
    
    
   /**
	 * 缓存导入选择器作用:将一堆的缓存配置。
	 */
	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++) {
                //2、导入对应的缓存配置
				imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
			}
			return imports;
		}

	}
    
}

RedisCacheConfiguration

  • 我使用了 Redis,这里会导入 RedisCacheConfiguration配置。
final class CacheConfigurations {

	private static final Map<CacheType, Class<?>> MAPPINGS;

	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);
        
        //2、导入 RedisCacheConfiguration
		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);
	}
    
    //2、导入对应的缓存,我这里用的是 Redis 缓存,对应的 RedisCacheConfiguration 配置被导入。
    static String getConfigurationClass(CacheType cacheType) {
		Class<?> configurationClass = MAPPINGS.get(cacheType);
		Assert.state(configurationClass != null, () -> "Unknown cache type " + cacheType);
		return configurationClass.getName();
	}
    
  	/**
	 * 创建默认的缓存配置如下:
	 * 		key的失效时间为:永久
	 *  	缓存默认支持 Null 值插入
	 * 		默认支持缓存key前缀
	 * 		默认key前缀的格式为:>[the actual cache name]
	 * 		key serializer 使用:{@link org.springframework.data.redis.serializer.StringRedisSerializer}
	 * 		value serializer 使用:{@link org.springframework.data.redis.serializer.JdkSerializationRedisSerializer}
	 * 		conversion service 使用这个缓存可以转换器:{@link DefaultFormattingConversionService} with {@link #registerDefaultConverters(ConverterRegistry) default} 
	 */
	public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) {

		DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

		registerDefaultConverters(conversionService);

		return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(),
				SerializationPair.fromSerializer(RedisSerializer.string()),
				SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
	}
}

RedisCacheManager

  • 默认自动配好了缓存管理器 RedisCacheManager (如果用户自定义了缓存管理器,则使用自定义的,不使用默认的)
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
	
    //3、导入 RedisCacheManager 缓存管理器。
	@Bean
	RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
			ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
			ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
			RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
        // 3.1 构建缓存配置
		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());
	}

    //3.2 决定缓存配置的方法
	private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
			CacheProperties cacheProperties,
			ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
			ClassLoader classLoader) {
        //3.3 判断是否有用户自己创建的配置,如果有就用自定义的配置,如果没有则创建默认配置
		return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
	}
    
	//3.4 创建默认配置方法:决定默认配置具体配置了什么
	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) {
            // 设置 ttl 失效时间
			config = config.entryTtl(redisProperties.getTimeToLive());
		}
		if (redisProperties.getKeyPrefix() != null) {
            // 设置 缓存key前缀
			config = config.prefixKeysWith(redisProperties.getKeyPrefix());
		}
		if (!redisProperties.isCacheNullValues()) {
            // 设置 是否禁用缓存Null值
			config = config.disableCachingNullValues();
		}
		if (!redisProperties.isUseKeyPrefix()) {
            // 设置 是否禁用缓存key前缀
			config = config.disableKeyPrefix();
		}
		return config;
	}

}

RedisCache

Cache 类如下图:

Cache类

RedisCache 实现类 如下图:

有一个get方法使用了 synchronized 关键字 (重写了 Cache 接口的 get方法)

RedisCache 实现类

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值