当前位置: 首页>后端>正文

Spring Cache接入文档

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>

配置

  1. 如果缓存实现使用redis,配置好redis即可
    配置示例:
spring:
  redis:
    database: ${redis.database}
    host: ${redis.host}
    port: ${redis.port}
    password: ${redis.password}
    pool:
      max-active: ${redis.maxActive}
  1. 配置CacheManager

可以配置不同的CacheManager,指定不同的ttl以及序列化策略,然后在使用注解的过程中可以指定cacheManagerspring-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;
    }

使用示例

  1. @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");
    }
  1. @CachePut更新缓存
    核心参数释义(参考@Cacheable):
    代码示例:
@Override
@CachePut(value = "book", key = "#book.isbn")
public Book saveOrUpdate(Book book) {
    book.setTitle(book.getTitle() + "1");
    return book;
}
  1. @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;
}
  1. @CacheConfig类级别配置缓存

类级别配置之后,类中的方法不需要重复配置

@CacheConfig(cacheManager = "cacheManager")
public class SimpleBookServiceImpl{

}
  1. @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");
}
  1. 自定义的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-cacheevict方法对键进行清除。

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),缺乏对缓存信息的统计支持(通过prometheusredis集群本身进行监控粒度相对就比较粗一些)。在方法级别配置TTL只能通过配置不同的cacheManager来实现,也可以使用@CacheConfig注解在类级别进行控制(不过总的来说,对TTL的设置会遵循分组原则,不会泛泛的设置,所以做成这样也是可以接受的)。


https://www.xamrdz.com/backend/38c1934949.html

相关文章: