廊虞

一如既往个人网站

SpringBoot 缓存注解的使用

最近比较忙,没时间更新了。上一篇文章我说了如何使用Redis做缓存,文末我稍微提到了SpringBoot对缓存的支持。本篇文章就针对SpringBoot说一下如何使用。

1、SpringBoot对缓存的支持

SpringBoot对缓存的支持我们需要引入包:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

<!-- 如果需要集成redis,需要再加入redis包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.4.2</version>
        </dependency>

缓存的支持是依靠接口:org.springframework.cache.annotation.CachingConfigurer的实现。所以我们使用SpringBoot自定义缓存只需要实现CachingConfigurer接口,并给出合理的实现即可。所以我们通常使用缓存如Ecache,Redis等较好的缓存框架都是已经实现了的。默认情况下SpringBoot同样是使用本地缓存,可以通过配置文件配置缓存配置项。具体如何配置请参考我的上篇文章:如何使用Redis做缓存
SpringBoot为我们做了很多事情,我们使用缓存只需要了解3个注解:@Cacheable, @CachePut, @CacheEvict。Cacheable作用是读取缓存,CachePut是放置缓存,CacheEvict作用是清除缓存。

2、Cacheable注解

这个注解我们会相对熟悉,在上一篇文章中我们就说过这个注解的使用,这里再复制粘贴一下:

/**
 * 就直接占着源码说了
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
	/**
     * 设置使用换成的名称,这两个值是一样的,我们通过这个值来区分不同缓存的配置
     * 比如我们可以设置不同的cacheName来设置缓存时间、设置不同的key生成策略等。
     */
	@AliasFor("cacheNames")
	String[] value() default {};
	@AliasFor("value")
	String[] cacheNames() default {};
	/**
     * 设置key生成策略,支持spel表达式,且存在root方法,可以通过#root.method,#root.target等使用root对象
     * 同样可以固定缓存key为固定字符串。
     * 如果不设置,SpringBoot提供默认的key生成策略。
     */
	String key() default "";
	/**
     * 在注解中指定key生成器
     */
	String keyGenerator() default "";
	/**
     * 在注解中制定此注解使用的缓存管理器
     */    
	String cacheManager() default "";
	/**
     * 在注解中指定此注解的缓存解析器,和缓存管理器互斥
     */
	String cacheResolver() default "";
	/**
     * 设置使用缓存条件,使用Spel表达式解析。如果表达式返回false,则这次执行不走缓存逻辑
     */
	String condition() default "";
	/**
     * 设置不缓存条件,Spel表达式。如果表达式返回true,则不对缓存结果进行缓存。
     * 这个和condition()的区别是在于执行时机不同,condition方法是在执行方法前调用,而unless方法是在执行目标方法后调用。
     */
	String unless() default "";
	/**
     * 采用同步的方式,默认false
     */
	boolean sync() default false;
}

此注解主要描述缓存的使用策略

举个栗子:

    public static final String D1 = "cache_1d";

    @Cacheable(value = CacheTimes.D1, key = "#root.methodName", unless = "#result == null || #result.size() < 1", condition = "#skip != null")
    public List<String> getList(String skip) {
        return Arrays.stream(UUID.randomUUID().toString().split("-")).collect(Collectors.toList());
    }

上述例子使用了@Cacheable注解,解析一下:

  1. value="cache_1d"是我定义的缓存值,这个配置设置了缓存1天。
  2. 自定义缓存key,key为方法名。同样可以使用keyGenerator设置key生成策略,不过我觉得使用spel表达式更加灵活,如果使用keyGenerator,keyGenerator的实现类需要实现org.springframework.cache.interceptor.KeyGenerator接口,具体的实现方法实现在generate方法中。
  3. unless的意思就是如果返回结果是null数组或数组大小是0,则不缓存此结果。
  4. condition则判定了是否走换成逻辑,如果skip是null,即condition是false,就不去读取缓存,而是直接执行目标方法。
  5. 如过有需求,你们可以指定cacheManager或cacheResolver。比如默认使用的是RedisCacheManager,然而某个缓存不需要使用Redis即可,可以单独使用Ecache,则可以在注解中指定Ecache的cachemanager。

3、CachePut注解

上面我们说CachePut注解作用是放置缓存。有点别扭,意思就是CachePut注解能够设置一些满足条件的缓存。虽然说我们通常用它来更新缓存(假如使用Redis做缓存,可以用它来设置redis的值。虽然能实现,但是还是不建议用这个去设置Redis的值哈,因为那样使用违反了注解的本身的意思,别人看了种以为它是缓存咋办),但我认为不能说成更新缓存,更新的意思是必须要有然后改变其值。
CachePut的源码和Cacheable基本一致

public @interface CachePut {
	@AliasFor("cacheNames")
	String[] value() default {};
	@AliasFor("value")
	String[] cacheNames() default {};

	String key() default "";
	String condition() default "";
	String unless() default "";
    
	String keyGenerator() default "";
	String cacheManager() default "";
	String cacheResolver() default "";
}

同理Cacheable,CachePut注解生效是在满足condition()true和unless()false的情况。

用法举栗子

    @CachePut(value = CacheTimes.D1, key = "#root.methodName", unless = "#result == null || #result.size() < 1", condition = "#condition" )
    public List<String> setListCache(List<String> list, boolean condition){
        if(list != null && list.size() > 0){
            return list;
        }
        return Arrays.stream(UUID.randomUUID().toString().split("-")).collect(Collectors.toList());
    }

上例中unless和condition和Cacheable用法一致,不同的就是CachePut注解的方法被调用时不会去读取缓存中存储的数据,只是在调用结束判断是否将数据写入缓存。即满足参数condition=true,return value is not NullList,就会以key=setListCache将执行结果存入缓存中。
如果key设置和Cacheable缓存的key相同,那么方法调用结束就会更新缓存。

  • 千奇百怪:使用CachePut给Redis赋值其实就是凑凑字数,既然是缓存的注解,那么咱们还是只用来做缓存更好,并且,缓存也不一定用的就是Redis,哈哈哈哈哈哈哈
    @CachePut(value = CacheTimes.D1, key = "#key")
    public Object setKV(String key, Object value){
        return value;
    }

如果你这样使用了,那么应要注意你的value这个缓存的过期时间,和CacheEvict注解的使用。。。。

4、CacheEvict注解

CacheEvict注解在相对于前两个注解,多了两个属性:allEntries和beforeInvocation

public @interface CacheEvict {
    /** 是否删除所有缓存 */
	boolean allEntries() default false;
 	/** 在方法执行前/后进行删除缓存操作 */
	boolean beforeInvocation() default false;
    
	@AliasFor("cacheNames")
	String[] value() default {}; 
	@AliasFor("value")
	String[] cacheNames() default {};
	String key() default "";
	String condition() default "";
    
	String keyGenerator() default "";
	String cacheManager() default "";
	String cacheResolver() default "";
}

参数:allEntries

allEntries参数作用是删除所有缓存数据,默认是false,让我们指定key去删除匹配的缓存。详细看下面两个例子:

  • 删除全部缓存数据
    @CacheEvict(value = CacheTimes.D1, condition = "#condition", allEntries = true)
    public void clearCache(boolean condition){
        ……
    }

上面的方法的意思就是说当参数condition=true时,CacheEvict注解开始生效,因为allEntries=true,所以会删除value=CacheTimes.D1下所有的缓存数据。
比如之前在CacheTime.D1下设置了缓存:"a"="b","c"="c","d"="d",在调用了clearCache方法之后,上述缓存将全部被删除。

  • 指定key删除
    @CacheEvict(value = CacheTimes.D1, allEntries = false, key = "#key")
    public void clearCache(String key){

    }

如果在allEntries=false的情况下,CacheEvict将会删除制定key的键值。理应在指定key的情况下,allEntries应当为false;否则指定的key将无效。

那么当allEntries=true的时候,springboot是怎么判断应当删除哪条数据的呢?
这个我们要先搞清楚缓存的key生成策略:默认情况下,缓存会拿注解中的value值作为前缀+"::"+你自定义的key生成策略作为这个缓存的真实key。当我们调用CacheEvict注解中allEntries的值为true时,springboot就会根据上述的key生成策略去匹配缓存系统中的数据,即以注解中value值为前缀的key去删除。
那么就会存在这样一个问题:假如我们重写了上述的key生成策略,使得缓存在生成key时没有前缀,那么删除时会发生什么?
答案就是你想象的那样:会删除缓存系统中所有的数据,如果缓存使用的是redis,那么redis中所有的数据将被清空。

参数:beforeInvocation

既然是缓存,我们就应当更加全面的去操作缓存。那么假设我们对某个User表做了缓存,当添加数据时,我们使用Cacheable去设置缓存;读取时同样根据Cacheable策略去取出数据;当修改时,我们使用CachePut去更新缓存;当删除时,我们理应使用CacheEvict去删除缓存。这时就会存在一个问题:在调用方法删除某条id=123的用户记录时,由于业务原因出现异常(是否删除成功状态未知),那么这时,这个缓存我们应不应当清理。
这种情况下beforeInvocation给我们了选择。当beforeInvocation=true时,SpringBoot先进行缓存操作(先删除缓存)在执行方法逻辑。当beforeInvocation=false时,先执行方法业务逻辑,再删除缓存。(当然,业务正常执行的时候都无所谓)
当业务出现异常时,我们要么选择删除缓存,要么不删除。

  • 缓存操作在前(先删除缓存,再执行逻辑,不管是否异常,缓存已经清空)
@CacheEvict(value = CacheTimes.D1, condition = "#condition", beforeInvocation = true, allEntries = true)
public void clearCache(boolean condition){
    throw new RuntimeException("exception");
}
  • 缓存操作在后(默认,出现异常不删除缓存)
@CacheEvict(value = CacheTimes.D1, condition = "#condition",beforeInvocation = false, allEntries = true)
public void clearCache(boolean condition){
    throw new RuntimeException("exception");
}

5、Caching注解实现复杂缓存逻辑

缓存就是读写更新,那么上面三个注解已经够了,Caching是干什么的呢?

public @interface Caching {
	Cacheable[] cacheable() default {};
	CachePut[] put() default {};
	CacheEvict[] evict() default {};
}

Caching注解中包含了Cacheable、CachePut、CacheEvict三个注解。所以我们很应该能够想象得到,在Caching中能够使用多个缓存注解,主要是为了实现一些复杂的缓存逻辑,且不需要在多个方法上去实现。
这个没啥好说的,简洁明了。

6、常见的(我碰到的)业务实现使用。。。

1、对mysql某个表进行缓存

这种逻辑通常不会使用单机缓存,而经常使用一个单独的缓存系统:比如Redis、elasticsearch去作为缓存,因为要保持数据的一直性。

	class user{......}
	//存储数据库的同时,存储到缓存
    @CachePut(value = "user", condition = "#user != null", key = "#user.id")
    public User insert(User user){
        jdbc.insert(user)
        return user;
    }
	//读取,如果缓存过期,则重新查数据,重新写入缓存
	@Cacheable(value = "user", condition = "#userId != null", key = "#userId", unless = "#result != null")
    public User select(Integer userId){
        User user = jdbc.select(userId);
        return user;
    }
	//删除数据时同样清除缓存
    @CacheEvict(value = "user", condition = "#userId != null", key = "#userId", allEntries = false, beforeInvocation = true)
    public void delete(Integer userId){
		jdbc.delete(userId);
    }

2、对复杂查询逻辑进行缓存

通常我们一个restful接口会存在非常复杂的逻辑,导致接口请求时间过长,这时我们会考虑将接口中的数据做缓存,而不是每次进入都重新走一边业务。而业务多变,在整个接口中设置我们可能不仅仅去读取缓存,或许可以根据参数的不同我们需要同时实现缓存的更新和清理。demo如下:

    @Caching(
            cacheable = {
                    @Cacheable(value = CacheTimes.D1, key = "#root.methodName + #userId", condition = "#update == null", unless = "#result != null || #result.size() < 1"),
                    @Cacheable(value = CacheTimes.D7, key = "#root.methodName + #userId", condition = "#update == null", unless = "#result != null", cacheManager = "cacheManager")
            },
            put = {
                    @CachePut(value = CacheTimes.D1, key = "#root.methodName + #userId", condition = "#update != null && #update.size() > 0"),
                    @CachePut(value = CacheTimes.D7, key = "#root.methodName + #userId", condition = "#update != null", cacheManager = "cacheManager")
            }
    )
    public List<Object> recommend(String userId, List<Object> update){
        if(update != null && update.size > 0){
            return update;
        }
        List<Object> l1 = selectTable1(userId);
        List<Object> l2 = selectTable2(userId);
        List<Object> l3 = selectTable3(userId);
        List<Object> l4 = selectTable4(userId);
        List<Object> l5 = selectTable5(userId);
        List<Object> result = new ArrayList();
        result.addAll(l1);
        result.addAll(l2);
        result.addAll(l3);
        result.addAll(l4);
        result.addAll(l5);
        return converVo(result);
    }

1、如上述demo,当参数update不是null时我们直接返回了update的值,update不是null触发了CachePut注解,我们会更新两个缓存,一个是默认的缓存更新管理器,一个指定了缓存管理器。
2、那么当符合Cachebale注解的时候呢?我们上面定义了两个Cacheable注解,当两个都满足注解条件,而且两个缓存中的值是不同的,会报异常吗?当然不会,当出现两个的时候读取缓存时,SpringBoot会返回你第一个注解的值。
3、如果参数同时符合CachePut和Cacheable的时候呢?通过测试,我发现如果同时慢住这两个注解的条件,会以CachePut的优先级更高,所以1. 会更新缓存;2. 返回的结果是更新后的缓存。
4、如果我再加了个CacheEvict,会不会清空缓存?
当然会。

3、更多还在你的实践,有错误欢迎指导,有扩展欢迎大家评论。

好了,这个就到此结束了,欢迎大家给出意见。发表自己的建议,如需要补充,评论去也同时可见。拜拜。
想当初是打算一周一篇文章的,但是不太好搞啊,真的是脑袋本,面对着电脑想不出应该怎么说好这一句话,写了删,然后再写,诺,现在才写成这么样子,真羡慕那些文笔不错的同学,能够出口成章准确表达出自己的意思也能够让他人更好的理解。
给自己拉个赞吧:欢迎点赞关注和评论,更希望本篇文章你看完之后有所收获。

posted @ 2022-12-04 20:15  廊虞  阅读(528)  评论(0编辑  收藏  举报