spring boot 进阶(一)缓存
前言
我是在看尚硅谷雷丰阳雷神的视频学习的,然后之八章(前端那块我跳过了)是springboot的基础,已经说完了。从这章开始据说是springboot高级(其实我看了下,几乎都是整合中间件的)。反正他说是高级就高级吧。而我视频都看一半了也没半途而废的习惯,所以这里会看完这个教程,同样笔记也会记完。所以这算是一个新的系列吧~
大概的目录结构如下:
- SpringBoot与缓存。
- SpringBoot与消息。
- SpringBoot与检索。
- SpringBoot与任务。
- SpringBoot与安全。
- SpringBoot与分布式。
- SpringBoot与监控管理。
- SpringBoot与部署。
然后下面从缓存开始吧。
缓存
忘记在哪里看到的,现在的大数据特点是:海量,多样,实时
而针对这些特点,传统的关系型数据库肯定就满足不了需求啦,所以在非关系型数据库之前,就有了一个概念:缓存。
缓存的概念其实用的很多。比如说volatile关键字的作用:修改可见性。
当时在那里其实也用到了类似的原理:每一个线程引用主内存的一个变量然后保存在本地。用的时候直接用。普通变量是主内存中值改变了执行线程中不会更新。而volatile的作用就是被这个修饰的变量主内存中值改变了会同时告诉所有引用这个变量的执行线程(措词不标准请见谅,这里就是凭借音印象说的)。
然后这种在引用的时候顺便本地存一份。下次用直接从这里拿,这个过程就是缓存。
当然了当缓存的对象改变后一定要更新值。但是当这个对象没改变的时候直接从本地拿比重复的去查询数据库要好得多。
而正常项目中,读写比例8:2.所以读操作多的话,引入缓存是个很好的思路。
JSR107缓存规范
JSR 是 Java Specification Requests 的缩写,意思是 Java 提案规范。当然了这种实际使用中很少直接使用,但是其中概念是通用的:
javaCaching定义了五个核心接口:
- CacheProvider:管理控制多个CacheManager
- CacheManager:管理很多Cache
- Cache:类似Map的数据结构并临时存储多个key-value对。
- Entry:记录。就是一个k-v对。
- Expire:k-v对都有一个过期时间。
如果想使用这个缓存,要引入的依赖:
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
感兴趣的可以自己去看看源码,但是因为这个适配不太好,而且自身支持的缓存比较少。所以我们用这个并不多。所以这个就是简单的介绍一下五个接口就过了。
Spring缓存抽象
Spring从3.1开始定义了它自己的Cache和CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSP107)注解简化开发。其中几个重要的概念:
- CacheManager:缓存管理器,管理各种缓存(Cache)组件。
- Cache:缓存接口,定义缓存操作。其有多个实现。根据实现的不同可以执行不同的缓存技术。比如:RedisCache。EhCacheCache,ConcurrentMapCache等。
- @Cacheable:主要针对方法配置,根据方法的请求参数对结果进行缓存。(查询方法上加入这个注解)
- @CachePut:保证方法被调用,又希望结果被缓存。(这个是方法总会调用,结果重新被缓存。可以理解为更新)
- @CacheEvict:清空缓存。(删除方法上添加这个注解)
- @EnableCaching:开启基于注解的缓存。
- keyGenerator:缓存时key的生成策略
-
serialize:缓存时value序列化策略
截图
下面让我们简单使用下缓存:
-
一个基本带有数据库的测试项目
springBoot项目,这几个crud方法都是测通了的 - 想使用缓存注解,一定要在启动类上加@EnableCaching注解开启这个功能
- 注解的使用:
- @Cacheable :将方法的运行结果进行缓存,以后再要相同的数据直接从缓存中读取,不用调用方法了。
需要注意的是上面说了实际缓存是在Cache组件中。而Cache受管制与CacheManager。一个CacheManager管理多个Cache组件。所以每个Cache组件要有自己的名字。所以在每次存储是要告诉缓存这个kv对是放在哪个组件中的。同时我们也可以以k-v对的形式放入(指定key),当然了如果我们不指定key会默认使用方法的参数的值。简单说一下这个注解中的参数:- cacheNames/value:指定缓存组件的名称,数组的形式,可以指定多个cache名。
- key:缓存数据使用的key,可以指定,不指定默认是方法参数的值。也可以用spEl表达式来写,语法如下图。
- keyGenerator:key的生成器,可以自己指定key的生成器的组件id(key和keyGenerator二选一使用)
- cacheManager/cacheResolver:指定缓存管理器/解析器。这两个注解也是二选一的
- condition:符合指定条件的情况下才缓存。
- unless:否定缓存。当unless指定的条件是false才保存。可以获取方法的结果判断
-
sync:是否使用异步模式。默认是false-同步。可以改为true变成异步的
这个注解的参数是支持spel表达式的,如下:
cache中spEL表达式
- @Cacheable :将方法的运行结果进行缓存,以后再要相同的数据直接从缓存中读取,不用调用方法了。
说了那么多,我们在项目中测试一下,在一个查询方法上加个缓存注解。这里用最简版,只要指定cache不报错就行。如下页面准备:
准备测试
当我点击接口访问后,走了数据库
在数据库中查询出了id是8的数据
重点是接下来,我再次查询id是8的user信息:
确定走了缓存
至此可以确定,我们对查询结果的缓存是成功了的。
既然我们缓存成功了,下一步就是对于缓存的原理的分析啦。毕竟知其然是表面,只能让我们对付用,知其所以然才是本质,让我们能用的更顺手和调试更方便。
缓存实现的原理
首先cache也是自动配置的,所以仍然要去自动配置的包去找一下:
cache自动配置包下目录
如图我们看到这个cache中配置不多。但是除了一个autoConfiguration自动配置类外,还有11个configuration这个是什么玩意儿?其实我们看名字是能猜出来大概是不同的缓存技术的配置类。随便点进去一个看看就能看到这些配置类上是有注解的,而且这些类有个共同点:
@ConditionalOnMissingBean(CacheManager.class)
所有的类上都有这个注解,所以也就是说可以保证最终只有一个bean被注册成功。
什么时候生效
这些配置类都实现CacheManager
其实我们可以在配置文件中配置:
debug=true
打印一下哪些类生效了哪些没生效。
默认情况下simple生效了
因为之前已经确定只有一个生效了,所以不用看别的了,别的那几个xxcache都没生效。
而这个类生效了的话,我们去源码看看这个配置类干啥了:
注入ConcurrentMapCacheManager cacheManager
其实这个源码很容易看懂,就是向容器里注入了一个名称是cacheManager的ConcurrentMapCacheManager类型的bean(因为没起名字所以默认方法名。其实这个能看出来是父子类吧?)。
继续说上面创建了一个ConcurrentMapCacheManager类型的bean。我们点进这个类去看看这个类是什么?
ConcurrentMapCacheManager源码
其实ConcurrentMapCacheManager这个类还是能看懂的,里面有个线程安全的map。存的key是cache名(猜的),value是cache对象。这个我们因为之前就说过这个了,所以我觉得很容易能猜出来。
然后方法大多数也都是本类的方法,仔细顺着看也差不多能看懂,超出本类内容的也就最后三个方法:
这里存取缓存的时候涉及到了一个新的类ConcurrentMapCache。其实看前缀就能猜的差不多,因为cache我们之前说了具体的存储还是k-v对。这个是一个manager,管理多个cache。一个cache又管理多个kv对。着不就是套娃嘛!
其实这一块代码应该还是比较顺的,也没那么复杂。刚刚分析了半天,下面我们可以用断点的方式,亲眼见证一下这个缓存代码的走向:
代码还是上面的代码。我们在刚刚getCache这个方法上打个断点。然后访问接口。第一步就走到这个方法:
第一次访问是null
发现没有这个名称的cache,在确定没有(这里用的双重检查锁判空的)的时候创建一个叫做这个名字的cache并且里面没有元素。
创建cache
再把这个cache添加到管理器
把这个cache添加到管理器中
下面顺着代码一步一步走,该进方法就进,进错了出来就完事了,反正是到了下一个关键点:
自动生成key
回忆下上面说的不指定key默认就是方法参数~~~
去cache中找对应的key
注意这个方法是ConcurrentMapCache中的,其实就是看这个key有没有值。如果有值就直接返回了,没值再去数据库查询。逻辑还是很简单的。
判断有没有值,有直接取没有去方法中查询
总结一句话:@Cacheable标注的方法执行之前先检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,如果没有就去运行方法并将参数放入缓存中。如果有直接把结果返回。
至此,这个源码算是跟着走一遍了。反正我看完是有点飘了,甚至觉得我自己都能写一个缓存了,哈哈。开个小玩笑,这里我看到居然有人问服务器重启缓存还在不在了。这里的缓存不使用任何中间件(比如redis,es之类的)我感觉其实和项目中自己创建了个map差不多,其实有很多时候我们也不知不觉自己实现了这种缓存功能呢。
上面简单说了下原理,继续说使用吧:
刚说了这个key默认就是参数名。但是也可以自定义key的生成策略。写也比较简单,如下代码:
自己实现的方式:
自己实现key生成策略
我这里只是做了个demo,大家也可以自己想实现策略的。然后把这个bean指定到之前的注解上:
image.png
然后重新运行起来看一下key是什么:
这是什么鬼?
咳咳,拼接的时候有点小失误,拼出来的东西和我想得不太一样。不过这个不重要。重要的是我们自定义key的生成策略确实起作用了。
下面说下一个注解:@CachePut
@CachePut:这个注解和上一个注解@Cacheable调用的时机不一样。这个会先调用方法,然后把运行的结果存到缓存中。
正常来讲这个肯定是要和@Cacheable联合使用的。一个正常的逻辑是查询后将数据缓存起来。然后取的时候直接从缓存中取。但是如果这个对象发生改变了则要更新缓存。所以将这个对象对应的key的修改方法上指定这个注解。有如下几种方式:
存储和更新在一个key上
@CacheEvict:删除缓存。比如这条数据我们从数据库中删除,那么把这个缓存也删除了得了。反正什么时候用开心就好,重点是这个用法,同上面两个一样,指定cache的名字,指定key 的名称,就删除了。
使用方法
我这里反正是查询一次有了缓存,再查寻发现不用存数据库证明缓存确实生效了,然后调用del删除这个id。再查询发现又要走数据库了。说明确实删除成功!
当然这个删除缓存有一些特有的参数:allEntries这个参数是所有的k-v对(指定的cache中)。意思是是否删除所有的,默认是false。如果是true的话则会清空这个cache的所有缓存。
删除全部kv
还有一个截图中最后那个:beforeInvocation 是不是在方法执行之前执行。默认是false。如果改成true会在方法执行前执行。
这个的用法是如果这个方法代码出错了,那么就不会清除缓存了。但是如果在方法执行前就不管方法执行怎么样都会清楚。
@Caching:中间可以指定多个cache注解。我觉得可以理解为组合指令吧。如下demo:
组合指令
@CacheConfig:这个注解是写在类上的,可以指定这个类中所有的注解都按照公共配置来配置。作用就是抽取缓存的公共配置。
缓存中间件
上面我们也试过了,默认的化springBoot的缓存是simpleCache,但是正常我们工作中缓存都是放在了缓存中间件中的,所以这里说一下缓存中间件,常用的redis,ehcache,memcached等。这件说下用的比较多的redis。
关于redis的基本使用我之前已经学完了,所以这里直接用了。
- 引入redis依赖
之前都说了spring-data几乎都是有一个场景启动器的,所以我们只要引入这个场景启动器就行了,我是去maven仓库找的依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
在项目中引入依赖后,我们一步到位一点,直接看看这个redis自动配置给我们干了什么事,因为所有的自动配置格式都是XXXAutoConfiguration。所以直接找这个类:
redis自动配置类
redis自动配置类源码
简单的看下就知道了这个类生效条件是有一个RedisOperations.class类,估计有这个类就证明引入了这个redis依赖。
然后里面有俩类,都是不存在才生效,也就意味着我们可以自己写这个bean。一个是redisTemplate,一个是stringRedisTemplate。
其实这个XXXTempate在springboot中也是很常见的一个东西。就是模板嘛。一会儿我们可以试着用一下。
-
配置配置文件
项目中想使用redis。还是要做一些基本的配置的。比如redis的地址,端口,用户名,密码之类的。关于redis可以配置什么,怎么配置也可以自己去配置文件中找一下,如下截图:
redis配置内容
就像图中的属性我们都可以配置,不过大家可以看到有一些默认配置:比如端口6379,还有地址是localhost。又因为我的redis是最基本的redis,这些默认配置都ok,还没有密码。。所以理论上什么都不配置就可以使用。。。所以我这里直接用了。。
-
使用redisTemplate/stringRedisTemplate
第二个很明显就是操作字符串的。第一个是所有类型的。我们只要直接注入就可以使用了。如下代码:
注入redisTemplate
这里有很重要的一点!!!我们在之前注入bean的时候看到了注入的是一个两个泛型都是Object的RedisTemplate<Object, Object>。所以这里引用也要这么用,如果泛型不对就会报错说找不到这个bean的。当然了也可以象我这样省略不写。虽然会有警告。。
这个代码的运行结果:
按照预期的实现了
事实证明这个redis的操作就这么完成了。再看下我们都做了什么:
- 导包
- 因为配置都是基本的,所以没写配置
- 测试的时候直接注入使用了
简单来说就是导包即用,这个简直方便的吓人。。这里只测试了字符串操作,其实也可以试试别的数据格式。这里因为用着麻烦所以我就不说了。但是因为我上面偷懒用的字符串模板所以有个问题没暴露,这个说下这个问题:
redisTemplate模板使用问题
问题如下,获取和存储正常,但是存进redis中的东西莫名其妙多了点啥,虽然不影响使用但是也看着好不顺眼。而且这个这个前缀第一反应应该就是乱码,但是这个因为后面的内容是对的,所以应该不是乱码,第二个会让其变得这么奇怪的可能也就序列化了。
点进这个redisTemplate类就会发现一进去就能看到个默认序列化的属性,如下图:
默认序列化
这里因为一张图截不下来所以说下默认的是用的jdk的序列化:
if (defaultSerializer == null) {
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
虽然名头挺大,jdk自带的,但是我们平时用json足以说明它自带的不好用啊,所以这里我们可以自己注入bean,并且设置为我们想要的设置。如下代码:
大家记得当时系统自动注入的那个redisTemplate是不存在才会注入,我自己写了以后会注入我这个不会注入之前那个了。代码是我完全copy之前那个的。就加了一句话,指定这个序列化的方式了。然后运行:
指定序列化后结果
虽然还有点小问题,但是怎么也比之前的那个好了。。。而且也说明之前那个类似乱码的东西就是序列化的锅。至于这个双双引号。。。可能是我把字符串再次序列化的原因?我怀疑这个还是序列化方式的问题,不过我这个项目没引入gson或者fastjson甚至libjson之类的。。。先不管了。
毕竟说redis的主旨也不是为了简单使用的,回归正题,说下缓存。
之前什么都没配置,默认缓存用的simpleCache。现在我们引入了redis,根据之前的分析满足了redisCacheConfiguration的条件了,所以应该会引入redisCache了。而且因为cacheManager非空才生效的约束,所以应该只有这个redis的会生效。我们可以依然用debug=true的方式看一眼,我就不看了。
下面我们继续使用缓存,看看实际上的内容是不是按照我们的猜测存到了redis中,下面的测试代码:
除了打印语句外没有业务逻辑
如上测试代码,我们随便传几个名字测试下:
首先我们根据控制台打印确定了除了一次查询的名字剩下都不走入方法了,所以证明缓存生效了。这个时候我们看redis:
redis中数据
事实证明确实是存入到redis中了,而且这个cache名字也拼到了key上。名称-参数的方式来存储的。挺有意思的东西。
其实这里我为了偷懒又用了stringRedisTemplate了。因为用那个又要因为序列化很丑,反正这里自己改序列化方式就行了。
至此,这个简单的缓存的使用和原理就说完了,这里除了自带的缓存,就介绍了一些基本的概念,原理,只用redis做了一个举例。但是话说回来如果知道了原理想要使用也简单的很。大概的流程就是如果使用spring boot整合过可以自动注入的东西,就先去看xxxProperties,看看有什么属性方便自己配置,第二步看看自动配置类中bean在什么情况下使用。最后个性化配置使用就完了。这一套东西是一个教人学习的流程。当接入redis时这样,当接入mq也是这样,同理别的都是。授人与鱼不如授人与渔。这个远比单纯的教会api调用有用的多。
本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利!另外最近有点迷茫,甚至到了不知道学习是为了什么的地步。感觉现在之所以坚持每天刷题,看教学视频,做笔记已经变成了习惯。我不知道每天坚持学习有什么意义。但是却知道什么都不做更没可能。与大家共勉一句话吧:除非付出行动,否则口说无凭,没有人能通过祈祷试图改变人生!