接上篇,解读了Spring框架的缓存实现原理后,本文记录一些使用过程中的怎么办问题。
前言:
在没有引入其它类库,也没有自定义配置的情况下,一些默认的缓存实现:
- CacheManager:
org.springframework.cache.concurrent.ConcurrentMapCacheManager
- CacheResolver:
org.springframework.cache.interceptor.SimpleCacheResolver
- KeyGenerator:
org.springframework.cache.interceptor.SimpleKeyGenerator
目录
- 1、如何切换到Redis缓存?
- 1.1、在项目中引入redis库,会自动切换到`org.springframework.data.redis.cache.RedisCacheManager` .
- 1.2、上述方案是使用默认的Redis连接,和默认的序列化机制,如果想自定义连接,有2种方案:
- 2、如何为不同的cacheName,设置不同的缓存过期时间
- 3、如何为不同的key,设置不同的缓存过期时间
- 3.1、用另一个属性 cacheResolver
- 3.2、自行实现Cache类,并继承RedisCache
- 4、Cacheable注解的cacheName不支持yml配置读取,怎么办?
- 5、大家共用同一个Redis缓存,如何避免缓存名重复?
- 6、现有的注解不满足,我要增加新的自定义注解怎么办?
- 7、Redis缓存过期,不支持批量过期,怎么办?
- 8、缓存过期时,想执行一些代码怎么办?
- 8.1、使用AOP拦截,参考代码如下:
- 8.2、在CacheResolver里进行提前处理:
- 9、缓存过期,同时要清理其它的一些缓存怎么办?
- 10、设置了缓存过期,但是还是经常出现缓存不一致的问题?
1、如何切换到Redis缓存?
解答:
1.1、在项目中引入redis库,会自动切换到org.springframework.data.redis.cache.RedisCacheManager
.
这个Bean是在 org.springframework.boot.autoconfigure.cache.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());
}
1.2、上述方案是使用默认的Redis连接,和默认的序列化机制,如果想自定义连接,有2种方案:
- 方案1:自己定义Bean,例如在自己的类里写如下代码:
@Bean
RedisCacheManager createRedisCacheManager(Environment env) {
RedisConnectionFactory redisConnectionFactory = initConnection(env); // 从env配置里读取自定义配置,生成Factory
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(createRedisSerializer()))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.entryTtl(Duration.ofSeconds(86400)) // 默认缓存1天
.computePrefixWith(cacheName -> MikeCache.PREFIX + cacheName + ":");// 拼接全局统一的缓存key前缀
return new RedisCacheManager(redisCacheWriter, defaultConfig);
}
注:也可以定义为@Bean(value = "nameXXX", autowireCandidate = false)
,然后指定@Cacheable注解的cacheManager属性:@Cacheable(cacheNames = "beinet", cacheManager = "nameXXX")
- 方案2:不修改默认Bean,自己定义一个类,实现
org.springframework.cache.annotation.CachingConfigurer
例如:
@Component
public class BeinetCachingConfigurer implements CachingConfigurer {
@Override
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
@Override
public CacheResolver cacheResolver() {
return null;
}
@Override
public KeyGenerator keyGenerator() {
return null;
}
@Override
public CacheErrorHandler errorHandler() {
return null;
}
}
这样,这个类的CacheManager会初始化到 org.springframework.cache.interceptor.CacheInterceptor
里,从而被你的缓存所使用。
注意:此时@Autowired得到的Bean,并不是这里的 new ConcurrentMapCacheManager()
2、如何为不同的cacheName,设置不同的缓存过期时间
本节仅介绍使用RedisCacheManager,并设置过期:
在上面自定义CacheManager的代码里,就设置了缓存过期时间:.entryTtl(Duration.ofSeconds(86400)) // 默认缓存1天
但是,这是所有缓存使用同一个过期时间,如果我希望设置不同的缓存过期时间怎么办呢?
在CacheManager接口的定义里,有一个根据cacheName去获取缓存的方法:
public interface CacheManager {
@Nullable
Cache getCache(String name);
Collection<String> getCacheNames();
}
而根据前篇文章,我们知道读取缓存是通过 cacheManager.getCache
,参数为:注解的cacheNames属性。
而从RedisCacheManager
的实现里,它可以为每个cacheName分配了一个RedisCacheConfiguration:
public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
this(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation);
Assert.notNull(initialCacheConfigurations, "InitialCacheConfigurations must not be null!");
this.initialCacheConfiguration.putAll(initialCacheConfigurations);
}
所以,我们自己声明的RedisCacheManager
的Bean,为不同的cacheName分配不同的缓存过期时间即可,如:
@Bean("nameXXX")
RedisCacheManager createRedisCacheManager1(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("aaaCacheName", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(100)));
configs.put("bbbCacheName", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(200)));
return new RedisCacheManager(redisCacheWriter, configuration, configs);
}
3、如何为不同的key,设置不同的缓存过期时间
在上面,是根据不同的cacheName,来设置过期时间,可是,我希望根据不同的key来设置过期时间,或者根据不同的方法,或不同的参数,来设置过期时间呢?
这种情况下,cacheManager无能为力,因为在它的整个使用中,只能接触到调用者定义的cacheName,拿不到别的内容。
那么要根据key来设定不同的缓存时间,有如下2个方案:
3.1、用另一个属性 cacheResolver
这个属性对应一个接口,只有一个方法定义:
@FunctionalInterface
public interface CacheResolver {
Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context);
}
在前一篇文章里,我们知道,读写缓存,是通过CacheResolver.resolveCaches
方法得到最终的Cache,只不过Spring提供的CacheResolver实现,都是通过内置的CacheManager来实现。
这个CacheOperationInvocationContext
参数,可以得到Method对象,有方法名,方法参数,也能getOperation得到@Cacheable
注解上的内容,
那么我们可以自己写一个实现,在方法里去解析,返回需要的Cache:
public class MikeCacheResolver implements CacheResolver {
private final CacheManager cacheManager;
public MikeCacheResolver(CacheManager cacheManager, String applicationName) {
this.cacheManager = cacheManager;
}
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Cacheable annotation = context.getMethod().getAnnotation(Cacheable.class);
BasicOperation operation = context.getOperation();
if (operation instanceof CacheableOperation) {
}
Collection<Cache> ret = new ArrayList<>();
// 这里根据注解 或 方法得到的cacheName,去getCache,再返回不同的过期时间的Cache
ret.add(cacheManager.getCache("xxx"));
return ret;
}
注意:
因为Spring不是通过Bean读取默认的CacheResolver,
因此,需要通过上面内容里的 CachingConfigurer接口实现,来返回我们写的这个CacheResolver,
或者直接写在@Cacheable
注解的cacheResolver属性里。
3.2、自行实现Cache类,并继承RedisCache
根据RedisCacheManager
的实现,它读写缓存都是通过RedisCache
的,所以,我们重写一下它的缓存方法即可,当然,因为RedisCache
是在RedisCacheManager
里进行初始化的,所以也要重写RedisCacheManager
:
@Component
public class MikeRedisCacheManager extends RedisCacheManager {
private final RedisCacheWriter cacheWriter;
private final RedisCacheConfiguration defaultCacheConfig;
public MikeRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
this.cacheWriter = cacheWriter;
this.defaultCacheConfig = defaultCacheConfiguration;
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
return new MikeRedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}
}
public class MikeRedisCache extends RedisCache {
private final String name;
private final RedisCacheWriter cacheWriter;
protected MikeRedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(name, cacheWriter, cacheConfig);
this.name = name;
this.cacheWriter = cacheWriter;
}
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
if (!isAllowNullValues() && cacheValue == null) {
throw new IllegalArgumentException();
}
Duration duration = Duration.ofSeconds(100);
cacheWriter.put(name, serializeCacheKey(createCacheKey(key)), serializeCacheValue(cacheValue), duration);
}
}
4、Cacheable注解的cacheName不支持yml配置读取,怎么办?
Spring的缓存注解是不支持读取配置的,例如:@Cacheable(cacheNames = "beinet-${spring.application.name:UNKNOWN}")
,
最终就是直接把 这一整个字符串作为Redis的Key的一部分进行储存。
根据本文上面描述的,无法通过CacheManager进行处理,只能在CacheResolver里处理,
在获取Cache里,可以拿到注解信息并处理,例如:
@Component("beinetCacheResolver")
public class BeinetCacheResolver implements CacheResolver {
private CacheManager cacheManager;
private Environment env;
public BeinetCacheResolver(CacheManager cacheManager, Environment env) {
this.cacheManager = cacheManager;
this.env = env;
}
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
CacheOperation operation = (CacheOperation) context.getOperation();
Set<Cache> resolvedCaches = new HashSet<>();
Set<String> names = operation.getCacheNames();
for (String name : names) {
String resolvedName = env.resolvePlaceholders(name); // 解析里面的占位符 ${xx}
resolvedCaches.add(cacheManager.getCache(resolvedName));
}
return resolvedCaches;
}
}
5、大家共用同一个Redis缓存,如何避免缓存名重复?
在同一个项目里,可以通过配置不同的cacheNames,来避免重复,例如:@Cacheable(cacheNames = "beinet-cache1", key = "#id.toString() + '-' + #name")
@Cacheable(cacheNames = "beinet-cache2", key = "#id.toString() + '-' + #name")
上面2个注解,虽然key相同,但是因为cacheNames不同,因此是不会冲突的,
对应到Redis里的key分别为(假设方法参数的id为123, name为abc):beinet-cache1::123-abc
和 beinet-cache2::123-abc
所以,解决方案1,就是约定使用不同的cacheName。
但是,不同的项目去约定有时比较麻烦,而且每个地方都要配置,很容易遗漏……
其实,在本文上方第一个问题的1.2里,已经写了,.computePrefixWith(cacheName -> MikeCache.PREFIX + ":" + cacheName);
这样,每个项目只要定义一个地方就好,其它地方该怎么写还怎么写。
当然,我们是把它配置在底层框架里了。
btw:避免不同项目缓存key重复的更好方法就是,不同的项目,使用不同的Redis DB。
6、现有的注解不满足,我要增加新的自定义注解怎么办?
Spring默认提供了5个缓存注解,其中 CacheConfig
用于定义通用配置,Spring默认的SpringCacheAnnotationParser,只会处理如下4个注解,并且不支持扩展:
-
Cacheable
: 读缓存,不存在时,执行方法,并写入缓存; -
CachePut
: 不管有没有缓存,都要执行方法,并写入缓存; -
CacheEvict
: 删除缓存 -
Caching
: 支持在类或方法上添加多个缓存注解(上述3个注解)
如果以上4个注解无法满足你的需求,比如我重新定义了 MikeCacheable
,但是Caching
不支持我的 MikeCacheable
,怎么办?
方案1是继承AnnotationCacheOperationSource
,重写 getCacheOperations
方法;
方案2是自定义一个CacheAnnotationParser
接口实现类,并加入AnnotationCacheOperationSource.annotationParsers
下面是方案2的代码实现演示:
public class MikeCacheAnnotationParser implements CacheAnnotationParser, Serializable {
@Override
public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) {
return null;
}
@Override
public Collection<CacheOperation> parseCacheAnnotations(Method method) {
Collection<MikeCaching> cachings = AnnotatedElementUtils.findAllMergedAnnotations(method, MikeCaching.class);
if (cachings.isEmpty()) {
return null;
}
Collection<CacheOperation> ops = new ArrayList<>(1);
for (MikeCaching caching : cachings) {
if (caching == null || caching.cacheset() == null) {
continue;
}
for (MikeCacheSet cacheSet : caching.cacheset()) {
//Collection<Cacheable> cacheables = AnnotatedElementUtils.findAllMergedAnnotations(cacheSet.annotationType(), Cacheable.class);
ops.add(parseCacheableAnnotation(method, cacheSet));
}
}
return ops;
}
CacheableOperation parseCacheableAnnotation(AnnotatedElement ae, MikeCacheSet cacheable) {
CacheableOperation.Builder builder = new CacheableOperation.Builder();
builder.setName(ae.toString());
builder.setCacheNames(new String[0]);
builder.setCondition(cacheable.condition());
builder.setUnless(cacheable.unless());
builder.setKey(cacheable.key());
builder.setKeyGenerator("");
builder.setCacheManager("");
builder.setCacheResolver(cacheable.cacheResolver());
builder.setSync(cacheable.sync());
return builder.build();
}
}
然后再自己定义AnnotationCacheOperationSource
的Bean:
@Bean
public CacheOperationSource cacheOperationSource() {
return new AnnotationCacheOperationSource(new SpringCacheAnnotationParser(), new MikeCacheAnnotationParser());
}
7、Redis缓存过期,不支持批量过期,怎么办?
Redis是支持通配符的,比如 keys abc*def
是查找abc开头,def结束的所有key。
而且RedisCache
类里也有删除全部缓存的方法:
@Override
public void clear() {
byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);
cacheWriter.clean(name, pattern);
}
那么要实现批量删除 abc*def
,只能自己写了,参考本文上面3.2里的MikeRedisCache
类,只需要在那里重写一下evict方法即可:
public class MikeRedisCache extends RedisCache {
private final String name;
private final RedisCacheWriter cacheWriter;
private final ConversionService conversionService;
protected MikeRedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(name, cacheWriter, cacheConfig);
this.name = name;
this.cacheWriter = cacheWriter;
this.conversionService = cacheConfig.getConversionService();
}
/**
* 如果key包含 *, * 表示批量删除, 先用 keys xx* 查找,再一个个删除
*
* @param key 要删除的缓存key,支持 *
*/
@Override
public void evict(Object key) {
if (key.toString().indexOf('*') > -1) {
byte[] pattern = conversionService.convert(createCacheKey(key), byte[].class);
assert pattern != null;
cacheWriter.clean(name, pattern);
} else {
super.evict(key);
}
}
8、缓存过期时,想执行一些代码怎么办?
比如:希望在缓存过期时能自动续期,或发送一些事件通知,也有2种方案:
8.1、使用AOP拦截,参考代码如下:
@Aspect
public class MikeCacheEvictAop implements Ordered {
private final BeanFactory beanFactory;
public MikeCacheEvictAop(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Pointcut("@annotation(org.springframework.cache.annotation.CacheEvict)")
public void cachePointcut() {
// do nothing because @Pointcut
}
@Around("cachePointcut()")
public Object evictCallbackAround(ProceedingJoinPoint point) throws Throwable {
Method method = ((MethodSignature) point.getSignature()).getMethod();
CacheEvict evict = method.getAnnotation(CacheEvict.class);
Object ret = point.proceed();
// 这里读取注解信息,执行回调代码逻辑,也可以在本类里注入一些业务项目的接口处理
return ret;
}
8.2、在CacheResolver里进行提前处理:
如上面的MikeCacheResolver.resolveCaches
,在这个方法里,可以直接拿到 CacheOperation
实例。
但是要注意的是,这里不能算回调,它是在执行缓存操作之前进行
9、缓存过期,同时要清理其它的一些缓存怎么办?
比如品牌缓存过期,但是要同时删除品牌下的所有门店缓存,
此时,使用上面第8点的方法是不太方便达到这个目的的,因为Spring Cache的读写/清理缓存都被private方法封装了,没有暴露接口,而Cache暴露的只有cacheName、key,没有其它信息,很难提供CacheHelper这样的辅助操作类。
我的解决方案是定义一个组合注解,比如 MikeCacheEvict
:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@CacheEvict
public @interface MikeCacheEvict {
String key();
// Bean名称,如何获取关联的key
String relationKeyGenerator() default "";
String cacheResolver() default MikeCache.BEAN_NAME;
String condition() default "";
boolean allEntries() default false;
boolean beforeInvocation() default true;
}
然后在自定义的MikeCacheAnnotationParser
(参考第6点)里,添加如下逻辑:
public class MikeCacheAnnotationParser implements CacheAnnotationParser, Serializable {
@Override
public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) {
return null;
}
@Override
public Collection<CacheOperation> parseCacheAnnotations(Method method) {
Collection<CacheOperation> ops = parseReleationEvict(method);
return ops;
}
private Collection<CacheOperation> parseReleationEvict(Method method) {
Collection<MikeCacheEvict> cachings = AnnotatedElementUtils.findAllMergedAnnotations(method, MikeCacheEvict.class);
if (cachings.isEmpty()) {
return new ArrayList<>();
}
Collection<CacheOperation> ops = new ArrayList<>(1);
for (MikeCacheEvict cacheEvict : cachings) {
// 添加关联key的清理类
if (StringUtils.hasText(cacheEvict.relationKeyGenerator())) {
ops.add(parseCacheEvictAnnotation(method, cacheEvict, true));
}
}
return ops;
}
private CacheEvictOperation parseCacheEvictAnnotation(AnnotatedElement ae, MikeCacheEvict cacheEvict, boolean isReleation) {
CacheEvictOperation.Builder builder = new CacheEvictOperation.Builder();
builder.setName(ae.toString());
builder.setCacheNames();
builder.setCondition(cacheEvict.condition());
if (isReleation) {
builder.setKey("");
builder.setKeyGenerator(cacheEvict.relationKeyGenerator());
} else {
builder.setKey(cacheEvict.key());
builder.setKeyGenerator("");
}
builder.setCacheManager("");
builder.setCacheResolver(cacheEvict.cacheResolver());
builder.setCacheWide(cacheEvict.allEntries());
builder.setBeforeInvocation(cacheEvict.beforeInvocation());
return builder.build();
}
}
10、设置了缓存过期,但是还是经常出现缓存不一致的问题?
CacheEvict
注解有个属性 beforeInvocation
,默认值为false,表示在调用方法之后进行缓存清理。
如果设置为true,表示在调用方法之前进行缓存清理。
不推荐修改为true,因为清理后执行方法前,可能出现并发设置缓存,导致不一致。
另外,注解的方法本身内部如果调用了填充缓存的方法,也可能导致不一致。