spring-cache
接入文档
官方文档
https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/cache.html
https://spring.io/guides/gs/caching/
参考文档:https://my.oschina.net/dengfuwei/blog/1616221
spring-cache
简介
spring
自带的通用缓存框架,其内部集成了多种缓存实现(基于本地内存的caffeine
以及基于网络的redis
,还包括JCache
,EhCache
等等),配置CacheManager
选择具体的缓存实现。spring-cache
通过注解的方式非常透明方便的对已有的程序添加大量的缓存而不侵入已有的代码。其实现的原理类似于spring框架对事务的处理,也是使用aop
的方式来实现的。
spring boot
项目接入
添加maven依赖
<!--如果需要使用redis作为缓存,需要添加redis相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${version}</version>
</dependency>
配置
- 如果缓存实现使用
redis
,配置好redis
即可
配置示例:
spring:
redis:
database: ${redis.database}
host: ${redis.host}
port: ${redis.port}
password: ${redis.password}
pool:
max-active: ${redis.maxActive}
- 配置
CacheManager
可以配置不同的
CacheManager
,指定不同的ttl
以及序列化策略,然后在使用注解的过程中可以指定cacheManager
。spring-cache
默认使用的是::
服务作为key的前缀分隔符。可以通过computePrefixWith
自定义分隔符
// Spring cache set the default key prefix is `::`, replace of `:`
public static final String KEY_PREFIX = ":";
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* If config multiple `CacheManager`, this is the default
*
* @return
*/
@Bean("cacheManager")
@Primary
public CacheManager cacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
// Set value serializer
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
// Set key serializer
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.computePrefixWith(cacheName -> cacheName + KEY_PREFIX);
Map<String, RedisCacheConfiguration> map = new ConcurrentHashMap<>();
// Config multi cache `ttl`
// map.put("books", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(9999)));
// map.put("book", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(8888)));
RedisCacheManager rcm = RedisCacheManager.builder(this.redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.withInitialCacheConfigurations(map)
.build();
rcm.setTransactionAware(true);
return rcm;
}
/**
* You can through `cacheManager` attribute set custom cacheManager
*
* @return
*/
@Bean("shortCacheManager")
public CacheManager shortCacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(1));
RedisCacheManager rcm = RedisCacheManager.builder(this.redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
rcm.setTransactionAware(true);
return rcm;
}
使用示例
-
@Cacheable
添加缓存,主要应用到查询数据的方法上
核心参数释义:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
// cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
// 缓存的key,支持SpEL表达式。默认是使用所有参数及其计算的hashCode包装后的对象(SimpleKey)
String key() default "";
// 缓存key生成器,默认实现是SimpleKeyGenerator
String keyGenerator() default "";
// 指定使用哪个CacheManager
String cacheManager() default "";
// 缓存解析器
String cacheResolver() default "";
// 缓存的条件,支持SpEL表达式,当达到满足的条件时才缓存数据。在调用方法前后都会判断
String condition() default "";
// 满足条件时不更新缓存,支持SpEL表达式,只在调用方法后判断
String unless() default "";
// 回源到实际方法获取数据时,是否要保持同步,如果为false,调用的是Cache.get(key)方法;如果为true,调用的是Cache.get(key, Callable)方法
boolean sync() default false;
}
keyGenerator
可以自定义key的生成策略。condition
,unless
指定缓存的生成条件(支持SpEL表达式,支持bean)。cacheManager
指定缓存的实现方式。
代码示例:
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "_"
+ method.getName() + "_"
+ StringUtils.arrayToDelimitedString(params, "_");
}
}
@Bean
public KeyGenerator keyGenerator() {
return new CustomKeyGenerator();
}
// @Cacheable(value = "books", keyGenerator = "customKeyGenerator")
// @Cacheable(value = "books", key = "#isbn", condition = "getTarget().condition()", unless = "getTarget().unless(#result)")
// @Cacheable(value = "books", key = "#isbn", condition = "getTarget().condition(#root.target, #root.method, #root.args)",
// unless = "getTarget().unless(#result)")
// @Cacheable(value = "books", key = "#isbn", condition = "#root.target.condition(#root.target, #root.method, #root.args)",
// unless = "getTarget().unless(#result)")
@Cacheable(value = "books", key = "#isbn", condition = "@spELServiceImpl.condition()", unless = "@spELServiceImpl.unless()")
// @Cacheable(value = "books", key = "#isbn", cacheResolver = "multipleCacheResolver")
public Book getByIsbn(String isbn) {
//simulateSlowService();
return new Book(isbn, "Some book");
}
-
@CachePut
更新缓存
核心参数释义(参考@Cacheable
):
代码示例:
@Override
@CachePut(value = "book", key = "#book.isbn")
public Book saveOrUpdate(Book book) {
book.setTitle(book.getTitle() + "1");
return book;
}
-
@Caching
组合缓存
核心参数释义(参考@Cacheable
)::
代码示例:
@Caching(
put = {
@CachePut(value = "bookTitle", key = "#book.title"),
@CachePut(value = "bootIsbn", key = "#book.isbn")
}
)
public Book compositeSave(Book book) {
book.setTitle(book.getTitle() + "1");
book.setIsbn(book.getIsbn() + "1");
return book;
}
-
@CacheConfig
类级别配置缓存
类级别配置之后,类中的方法不需要重复配置
@CacheConfig(cacheManager = "cacheManager")
public class SimpleBookServiceImpl{
}
-
@CacheEvict
缓存失效
核心参数释义:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {
// ...相同属性说明请参考@Cacheable中的说明
// 是否要清除所有缓存的数据,为false时调用的是Cache.evict(key)方法;为true时调用的是Cache.clear()方法
boolean allEntries() default false;
// 调用方法之前或之后清除缓存
boolean beforeInvocation() default false;
}
代码示例:
@CacheEvict(value = "books", allEntries = true)
public void deleteAll() {
log.info("Delete all entries of books");
}
- 自定义的
CacheInvalid
注解
通过
@CacheEvict
注解使缓存失效,但是该注解有一个很大的缺陷性就是要么对指定cacheName
全部清除(allEntries
设置为true),要么只能清除单个key的缓存,无法对多个key的缓存进行管控。为了解决这个问题,参照@CacheEvict
的实现,自定义实现了一个CacheInvalid
注解,其支持对多个key及使用类似于redis
的key匹配模式对多个key进行清除(可以参考:https://github.com/spring-projects/spring-framework/issues/15586对该问题的讨论)。
核心参数释义:
/**
* @author chengliangpu
* @date 2022/1/21
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheInvalid {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
// 缓存的key,支持SpEL表达式。如果`pattern`为空,则使用key的SpEL返回值(支持数组列表以及单个字符串)
String key() default "";
// key的匹配模式,如果`pattern`不为空,则优先使用匹配模式,忽略key的SpEL返回
String pattern() default "";
String cacheManager() default "";
String condition() default "";
boolean beforeInvocation() default false;
}
核心实现代码CacheInvalidAspect
切面类:
其核心功能就是对使用了
@CacheInvalid
注解的方法进行拦截,使用spring自带的SpEL引擎对相关参数进行解析,最后调用spring-cache
的evict
方法对键进行清除。
package com.spring.cloud.test.caching.config;
import com.spring.cloud.test.caching.CacheInvalid;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Set;
/**
* @author chengliangpu
* @date 2022/1/21
*/
@Aspect
@Slf4j
@ConditionalOnBean(name = "redisTemplate")
public class CacheInvalidAspect {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private static final String RESULT_VARIABLE = "result";
public CacheInvalidAspect() {
}
@Around("@annotation(cacheInvalid)")
public Object cacheInvalid(ProceedingJoinPoint point, CacheInvalid cacheInvalid) {
CacheManager cacheManager = this.getCacheManager(cacheInvalid.cacheManager());
if (Objects.isNull(cacheManager)) {
log.error("Cannot find the cache manager which name is:{}", cacheInvalid.cacheManager());
throw new IllegalArgumentException(String.format("The cache manager not exist which name is:%s", cacheInvalid.cacheManager()));
}
Object retVal;
Object target = point.getTarget();
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
String condition = cacheInvalid.condition();
if (cacheInvalid.beforeInvocation()) {
if (this.getCondition(target, method, args, condition)) {
this.invalidCache(cacheInvalid, cacheManager, target, method, args);
}
// Invoke raw function
try {
retVal = point.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("Invoke CacheInvalid proxy raw method error", throwable.getCause());
}
} else {
try {
retVal = point.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("Invoke CacheInvalid proxy raw method error", throwable.getCause());
}
if (this.getCondition(target, method, args, condition, retVal)) {
this.invalidCache(cacheInvalid, cacheManager, target, method, args);
}
}
return retVal;
}
private CacheManager getCacheManager(String managerName) {
if (StringUtils.isEmpty(managerName)) {
return this.applicationContext.getBean(CacheManager.class);
} else {
Object obj = this.applicationContext.getBean(managerName);
if (obj instanceof CacheManager) {
return (CacheManager) obj;
} else {
return null;
}
}
}
private boolean getCondition(Object target, Method method, Object[] args, String condition) {
return this.getCondition(target, method, args, condition, null);
}
private boolean getCondition(Object target, Method method, Object[] args, String condition, Object result) {
if (StringUtils.isEmpty(condition)) {
return true;
}
Expression expression = EXPRESSION_PARSER.parseExpression(condition);
EvaluationContext context = this.getEvaluationContext(target, method, args);
if (Objects.nonNull(result)) {
context.setVariable(RESULT_VARIABLE, result);
}
Boolean retVal = expression.getValue(context, Boolean.class);
return !Objects.isNull(retVal) && retVal;
}
private void invalidCache(CacheInvalid cacheInvalid, CacheManager cacheManager, Object target, Method method, Object[] args) {
String[] cacheNames = cacheInvalid.value();
String pattern = cacheInvalid.pattern();
String keySpel = cacheInvalid.key();
for (String cacheName : cacheNames) {
Cache cache = cacheManager.getCache(cacheName);
if (Objects.isNull(cache)) {
log.info("Get cache:{} is null", cacheName);
continue;
}
if (!StringUtils.isEmpty(pattern)) {
pattern = cacheName + SpringCacheConfig.KEY_PREFIX + pattern;
Set<String> keys = this.redisTemplate.keys(pattern);
if (CollectionUtils.isEmpty(keys)) {
log.info("Get redis keys of pattern:{} is empty", pattern);
continue;
}
// Clear cache of keys
for (String key : keys) {
String key1 = key.substring(cacheName.length() + SpringCacheConfig.KEY_PREFIX.length());
cache.evict(key1);
}
} else {
Expression expression = EXPRESSION_PARSER.parseExpression(keySpel);
EvaluationContext context = this.getEvaluationContext(target, method, args);
ArrayList<String> keys = expression.getValue(context, ArrayList.class);
if (CollectionUtils.isEmpty(keys)) {
log.info("Resolve SpEL expression:{} to ArrayList is empty", keySpel);
continue;
}
for (String key : keys) {
if (key.contains(cacheName)) {
String key1 = key.substring(cacheName.length() + SpringCacheConfig.KEY_PREFIX.length());
cache.evict(key1);
} else {
cache.evict(key);
}
}
}
}
}
private EvaluationContext getEvaluationContext(Object target, Method method, Object[] args) {
// StandardEvaluationContext context = new StandardEvaluationContext(target);
// context.setBeanResolver(new BeanFactoryResolver(this.applicationContext));
//
// for (int i = 0; i < args.length; ++i) {
// MethodParameter methodParam = getMethodParameter(method, i);
// context.setVariable(methodParam.getParameterName(), args[i]);
// }
ExpressionRootObject rootObject = new ExpressionRootObject(method, args, target, target.getClass());
StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject, method, args, PARAMETER_NAME_DISCOVERER);
context.setBeanResolver(new BeanFactoryResolver(this.applicationContext));
return context;
}
public static MethodParameter getMethodParameter(Method method, int parameterIndex) {
MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex);
methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER);
return methodParameter;
}
}
核心实现代码ExpressionRootObject
类:
使用该类封装target object只是为了保持和使用其他注解的SpEL表达式一致
package com.spring.cloud.test.caching.config;
import lombok.Getter;
import java.lang.reflect.Method;
/**
* @author chengliangpu
* @date 2022/2/7
*/
public class ExpressionRootObject {
private final Method method;
private final Object[] args;
private final Object target;
private final Class<?> targetClass;
public ExpressionRootObject(Method method, Object[] args, Object target, Class<?> targetClass) {
this.method = method;
this.target = target;
this.targetClass = targetClass;
this.args = args;
}
public Method getMethod() {
return this.method;
}
public String getMethodName() {
return this.method.getName();
}
public Object[] getArgs() {
return this.args;
}
public Object getTarget() {
return this.target;
}
public Class<?> getTargetClass() {
return this.targetClass;
}
}
代码示例:
// @CacheInvalid(value = "books", pattern = "*1234*", cacheManager = "cacheManager", condition = "#result")
// @CacheInvalid(value = "books", key = "@spELServiceImpl.keys()", cacheManager = "cacheManager", condition = "#result")
// @CacheInvalid(value = "books", key = "#root.target.keys()", cacheManager = "cacheManager", condition = "#result")
@CacheInvalid(value = "books", key = "getTarget().keys()", cacheManager = "cacheManager", condition = "#result")
public boolean delete(String isbn) {
log.info("clear book cache:" + isbn);
return true;
}
public String[] keys(){
return new String[]{"isbn-1234","isbn-12345","isbn-4321"};
}
spring-cache
缺点
spring-cache
对缓存做了抽象,可以非常灵活的配置不同缓存。但是其相对于jetcache
框架而已少了对一级缓存的支持(当然也可以自己实现,参考:https://my.oschina.net/dengfuwei/blog/1616221),缺乏对缓存信息的统计支持(通过prometheus
对redis
集群本身进行监控粒度相对就比较粗一些)。在方法级别配置TTL
只能通过配置不同的cacheManager
来实现,也可以使用@CacheConfig
注解在类级别进行控制(不过总的来说,对TTL
的设置会遵循分组原则,不会泛泛的设置,所以做成这样也是可以接受的)。