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的设置会遵循分组原则,不会泛泛的设置,所以做成这样也是可以接受的)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容