关于SpringCache的一些思考
作者:互联网
关于SpringCache的一些思考
项目中用到了redis,同时在一些高频的查询方法使用redis作为缓存。但是因为没使用过SpringCache,当时看着网上的教程做了配置后,确实缓存生效了,就没有再管它。
突然项目经理跟我说redis中的缓存数据没有设置过期时间。看了下ttl都是-1,确实有问题。
为了更好的说清楚过程,下面就从头到位按顺序记录一下整个SpringCache的使用过程。而我之前又没有使用过SpringCache,所以会有很多低级的错误。如果你是对SpringCache比较熟悉的大佬,这文章应该不用往下看了。
1. 网上教程
首先记录一下我找到的一些教程。
基本如下:
-
最简单的,引入
spring-boot-starter-data-redis
,启动类上增加@EnableCaching
注解。在需要缓存的方法上使用@Cacheable
注解。这里说一下,Cacheable注解的cacheNames必须配,否则报错。 -
复杂一些,在上面的基础上,增加对redis的配置,如CacheManager、key,value的序列化规则等等。
示例代码:
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class RedisConfig {
/**
* 配置redis缓存管理器
*
* @param redisConnectionFactory Redis连接工厂
* @return 缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//通过 Config 对象即可对缓存进行自定义配置
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 序列化
.serializeKeysWith(keyPair())
// 设置 value 序列化
.serializeValuesWith(valuePair())
// 设置缓存前缀
.prefixCacheNameWith("cache:scrd:")
// 设置过期时间
.entryTtl(Duration.ofMinutes(30L));
// 返回 Redis 缓存管理器
return RedisCacheManager.builder(redisConnectionFactory)
.withCacheConfiguration("redis-test", cacheConfig)
.build();
}
/**
* 配置键序列化
*
* @return StringRedisSerializer
*/
private RedisSerializationContext.SerializationPair<String> keyPair() {
return RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer());
}
/**
* 配置值序列化,使用 GenericJackson2JsonRedisSerializer 替换默认序列化
*
* @return GenericJackson2JsonRedisSerializer
*/
private RedisSerializationContext.SerializationPair<Object> valuePair() {
return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
}
}
2. 项目背景
我们的项目中会调用第三方的系统查询产品数据,这些数据可以说就是不会改变的。我们当时决定将数据缓存并且过期时间设置为12个小时,其他的一般缓存默认过期时间为30分钟。
因为找到的SpringCache教程基本就是上面的使用方法,基本没有介绍如何配置不同的ttl,所以只能自己寻找思路了。
通过上面的教程,我猜想:CacheManager可以配置cacheName和RedisCacheConfiguration,那么我配置了2个CacheManager,分别设置不同的cacheName和RedisCacheConfiguration,是不是可以支持不同的过期时间。然后在需要的方法上指定对应配置的cacheNames。
比如下面的代码就是使用@Cacheable(cacheNames = "product")
按照这个思路我写了这样的代码:
/**
* 配置默认redis缓存管理器
*
* @param redisConnectionFactory Redis连接工厂
* @return 缓存管理器
*/
@Bean("defaultCacheManger")
public CacheManager defaultCacheManger(RedisConnectionFactory redisConnectionFactory) {
//通过 Config 对象即可对缓存进行自定义配置
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 序列化
.serializeKeysWith(keyPair())
// 设置 value 序列化
.serializeValuesWith(valuePair())
// 设置缓存前缀
.prefixCacheNameWith("cache:scrd:default:")
// 设置过期时间
.entryTtl(Duration.ofMinutes(10L));
// 返回 Redis 缓存管理器
return RedisCacheManager.builder(redisConnectionFactory)
.withCacheConfiguration("redis-test", cacheConfig)
.build();
}
/**
* 配置产品信息redis缓存管理器
*
* @param redisConnectionFactory Redis连接工厂
* @return 缓存管理器
*/
@Bean("productCacheManager")
public CacheManager productCacheManager(RedisConnectionFactory redisConnectionFactory) {
//通过 Config 对象即可对缓存进行自定义配置
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 序列化
.serializeKeysWith(keyPair())
// 设置 value 序列化
.serializeValuesWith(valuePair())
// 设置缓存前缀
.prefixCacheNameWith("cache:scrd:product:")
// 设置过期时间
.entryTtl(Duration.ofMinutes(720L));
// 返回 Redis 缓存管理器
return RedisCacheManager.builder(redisConnectionFactory)
.withCacheConfiguration("product", cacheConfig)
.build();
}
很遗憾,启动报错。
java.lang.IllegalStateException: No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.
大意是:找到多个CacheManager
,不知道注入哪一个,需要使用@Primary
指定一下默认使用哪一个。
果断给defaultCacheManger
加上@Primary
注解,正常启动。测试调用后,确实可以在redis看到数据。
3. 问题分析
上面看到的redis中数据其实有问题的,剧透一下:redis中key按照配置应该是以“cache:scrd:product:”开头,但是实际redis中缓存相关的key都是以“product开头”。因为“缓存”功能一直正常,所以我以为这是前缀的配置失效了。直到项目经理告诉我ttl有问题,我才会想起这个不一样的地方。
通过看源码,打断点,发现SpringCache在写数据进redis时,并没有使用我在productCacheManager中的相关配置,不仅是ttl的配置没有了,前缀的也没有了。
这让我不禁怀疑@Cacheable
注解的cacheNames参数是不是没办法关联到productCacheManager
。查看@Cacheable
注解的源码,发现可以直接配置cacheManager。然后就试了下直接配置cacheManager = "productCacheManager"
。
重启测试,发现这次可以正常使用我在productCacheManager
中的配置了。
4. 优化总结
这里重申一下:配置了cacheManager后,cacheNames还是要配置。因为我不配置就报错了。这个报错让我想到了,我一开始的思路或许是错误的。
从上面的过程来看,每个缓存必须指定对应的cacheName,但是可以不指定cacheManager。所以cacheManager应该可以由框架默认注入,而默认的cacheManager就是加了@Primary的那个Bean。这也是为什么手动指定cacheManager后可以正常。
再次观察CacheManager的配置代码。
查看配置代码中RedisCacheConfiguration是在withCacheConfiguration方法中使用的,参数还有cacheName。一开始我以为cacheName是给CacheManager设置的属性。查看源码发现该方法将RedisCacheConfiguration存在一个key为cacheName的Map中,所以RedisCacheConfiguration是和cacheName对应的。一个CacheManager可以有多个cacheName和RedisCacheConfiguration的组合。也就是说withCacheConfiguration方法可以多次调用。从而实现不同缓存,不同配置。使用的地方,通过cacheName来获取对应的配置。
withCacheConfiguration
源码:
/**
* @param cacheName
* @param cacheConfiguration
* @return this {@link RedisCacheManagerBuilder}.
* @since 2.2
*/
public RedisCacheManagerBuilder withCacheConfiguration(String cacheName,
RedisCacheConfiguration cacheConfiguration) {
Assert.notNull(cacheName, "CacheName must not be null!");
Assert.notNull(cacheConfiguration, "CacheConfiguration must not be null!");
this.initialCaches.put(cacheName, cacheConfiguration);
return this;
}
总结一下:SpringCache中一般只需要配置一个CacheManager,通过配置cacheName来指定特殊配置。除非是对接了多种缓存实现,比如即用redis,也用Ehcache。这个时候,就需要配置多个CacheManager,在使用的地方指定cacheManager。
调整后的代码:
/**
* 配置redis缓存管理器
*
* @param redisConnectionFactory Redis连接工厂
* @return 缓存管理器
*/
@Bean
public CacheManager cacheManger(RedisConnectionFactory redisConnectionFactory) {
//通过 Config 对象即可对缓存进行自定义配置
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 序列化
.serializeKeysWith(keyPair())
// 设置 value 序列化
.serializeValuesWith(valuePair())
// 设置缓存前缀
.prefixCacheNameWith("cache:scrd:")
// 设置过期时间
.entryTtl(Duration.ofMinutes(10L));
RedisCacheManager.RedisCacheManagerBuilder cacheManagerBuilder = RedisCacheManager.builder(redisConnectionFactory);
//设置默认的配置,当设置测cacheName没有配置的时候,使用默认配置
cacheManagerBuilder.cacheDefaults(cacheConfig);
//entryTtl方法不是在原有对象中修改配置
//而是会返回一个新的RedisCacheConfiguration对象,需要用cacheConfig接收。否则设置无效。
cacheConfig = cacheConfig.entryTtl(Duration.ofMinutes(720L));
//设置cacheName对呀的配置
cacheManagerBuilder.withCacheConfiguration("product", cacheConfig);
// 返回 Redis 缓存管理器
return cacheManagerBuilder.build();
}
标签:缓存,SpringCache,配置,redis,cacheName,RedisCacheConfiguration,关于,思考,CacheManager 来源: https://www.cnblogs.com/jimmyfan/p/14412363.html