最近比较忙,没时间更新了。上一篇文章我说了如何使用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、更多还在你的实践,有错误欢迎指导,有扩展欢迎大家评论。

好了,这个就到此结束了,欢迎大家给出意见。发表自己的建议,如需要补充,评论去也同时可见。拜拜。

想当初是打算一周一篇文章的,但是不太好搞啊,真的是脑袋本,面对着电脑想不出应该怎么说好这一句话,写了删,然后再写,诺,现在才写成这么样子,真羡慕那些文笔不错的同学,能够出口成章准确表达出自己的意思也能够让他人更好的理解。

给自己拉个赞吧:欢迎点赞关注和评论,更希望本篇文章你看完之后有所收获。

SpringBoot 缓存注解的使用的更多相关文章

  1. spring-boot -缓存注解

    缓存:商品信息放到缓存中间件中, 验证码几秒钟有效也是放在缓存中间件. 缓存规范 交互流程: 如果需要使用jRS107需要导入包: java.cache.cache-api JSR107提供的是接口, ...

  2. SpringBoot 缓存注解 与EhCache的使用

    在SpringBoot工程中配置EhCache缓存 1.在src/main/resources下新建ehcache.xml文件 eternal=true //缓存永久有效,false相反 maxEle ...

  3. springboot缓存注解——@Cacheable和@CacheConfig

    @Caching :制定多个缓存规则 @Cacheable 和 @CachePut 同时标注时 ,@CachePut导致还是会走方法跟数据库交互 //根据lastName查,并将结果缓存,并且可以用于 ...

  4. springboot缓存注解——@Cacheable

    @Cacheable: 1,方法运行之前,先查询Cache(缓存组件),按照cacheName指定的名字获取(CacheManager获取相应缓存) 第一次获取缓存如果没有Cache组件会自会自动创建 ...

  5. springboot缓存注解——@CacheEvict

    @CacheEvict:缓存清除 可以通过key指定清除的数据 如果不写默认参数的值 allEntries = true (是否删除该缓存名中所有数据,默认为false) beforeInvocati ...

  6. springboot缓存注解——@CachePut

    @CachePut:既调用方法,又更新缓存数据:修改了数据库的某个数据,同时又更新缓存 运行时机: 先调用目标方法 将目标方法的结果缓存起来 注意: @Cacheable的key不能用#result来 ...

  7. SpringBoot:缓存注解@Cacheable详解

    1.查看@Cacheable @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @ ...

  8. springboot:自定义缓存注解,实现生存时间需求

    需求背景:在使用springbot cache时,发现@cacheabe不能设置缓存时间,导致生成的缓存始终在redis中. 环境:springboot 2.1.5 + redis 解决办法:利用AO ...

  9. SpringBoot开启缓存注解

    https://blog.csdn.net/sanjay_f/article/details/47372967 https://www.cnblogs.com/lic309/p/4072848.htm ...

  10. SpringBoot缓存之redis--最简单的使用方式

    第一步:配置redis 这里使用的是yml类型的配置文件 mybatis: mapper-locations: classpath:mapping/*.xml spring: datasource: ...

随机推荐

  1. 运行python脚本报错SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \UXXXXXXXX escape

    运行python脚本报错 SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes  in position 2-3: ...

  2. (五).JavaScript的数组

    1. 数组 1.1 数组的基础 数组:同种或不同数据类型数据的有序集合 功能:同时存储多个数据 数据:常量 变量 表达式 数组 函数 对象 定义方式:字面量定义或者构造函数定义 字面量定义数组(本质上 ...

  3. idea更改JDK版本_Mac

    大致分为几个步骤: 在SDKs中添加JDK(已经添加过就不用添加了) 修改项目的Project SDK和Project language level(两个版本要一致) 修改Modules中的Sourc ...

  4. C语言声明与定义的区别

    转自:https://blog.csdn.net/gatieme/article/details/50640424 C++程序通常由许多文件组成,为了让多个文件访问相同的变量,C++区分了声明和定义. ...

  5. k8s ingress

    ingress   ingress为k8s集群中的服务提供了入口,可以提供复制均衡,ssl终止和基于名称的虚拟主机,再生产环境中,常用的ingress有Treafik,Nginx,HAProxy,Is ...

  6. 115、商城业务---分布式事务---使用Springboot提供的Seata解决分布式事务

    https://seata.io/zh-cn/ seata使用Seata AT模式控制分布式事务的步骤: 1.每一个想控制分布式事务的服务对应的数据库都需要创建一个UNDO_LOG 表 CREATE ...

  7. 多线程JUC练习

    package com.aliyun.test.learn; import java.util.concurrent.*; import java.util.concurrent.locks.Reen ...

  8. ArcEngine构造多部件

  9. 在POD的ENV中添加POD的信息

    主要用到的参数: - name POD_NAME volumeFrom: fieldRef: fieldPath:   metadata.name - name: POD_IP volumeFrom: ...

  10. archlinux 源配置 桌面美化 终端美化 常用软件 grub配置

    简介 本文讲对archlinux进行一些基础系统的配置.常用安装的安装,美化进行配置,先看一下美化后的效果吧 配置pacman和使用AUR(archlinuxcn源) archlinux采用滚动更新的 ...