前言
我是在看尚硅谷雷豐陽雷神的視頻學(xué)習(xí)的,然后之八章(前端那塊我跳過了)是springboot的基礎(chǔ),已經(jīng)說完了。從這章開始據(jù)說是springboot高級(jí)(其實(shí)我看了下,幾乎都是整合中間件的)。反正他說是高級(jí)就高級(jí)吧。而我視頻都看一半了也沒半途而廢的習(xí)慣,所以這里會(huì)看完這個(gè)教程,同樣筆記也會(huì)記完。所以這算是一個(gè)新的系列吧~
大概的目錄結(jié)構(gòu)如下:
- SpringBoot與緩存。
- SpringBoot與消息。
- SpringBoot與檢索。
- SpringBoot與任務(wù)。
- SpringBoot與安全。
- SpringBoot與分布式。
- SpringBoot與監(jiān)控管理。
- SpringBoot與部署。
然后下面從緩存開始吧。
緩存
忘記在哪里看到的,現(xiàn)在的大數(shù)據(jù)特點(diǎn)是:海量,多樣,實(shí)時(shí)
而針對(duì)這些特點(diǎn),傳統(tǒng)的關(guān)系型數(shù)據(jù)庫肯定就滿足不了需求啦,所以在非關(guān)系型數(shù)據(jù)庫之前,就有了一個(gè)概念:緩存。
緩存的概念其實(shí)用的很多。比如說volatile關(guān)鍵字的作用:修改可見性。
當(dāng)時(shí)在那里其實(shí)也用到了類似的原理:每一個(gè)線程引用主內(nèi)存的一個(gè)變量然后保存在本地。用的時(shí)候直接用。普通變量是主內(nèi)存中值改變了執(zhí)行線程中不會(huì)更新。而volatile的作用就是被這個(gè)修飾的變量主內(nèi)存中值改變了會(huì)同時(shí)告訴所有引用這個(gè)變量的執(zhí)行線程(措詞不標(biāo)準(zhǔn)請見諒,這里就是憑借音印象說的)。
然后這種在引用的時(shí)候順便本地存一份。下次用直接從這里拿,這個(gè)過程就是緩存。
當(dāng)然了當(dāng)緩存的對(duì)象改變后一定要更新值。但是當(dāng)這個(gè)對(duì)象沒改變的時(shí)候直接從本地拿比重復(fù)的去查詢數(shù)據(jù)庫要好得多。
而正常項(xiàng)目中,讀寫比例8:2.所以讀操作多的話,引入緩存是個(gè)很好的思路。
JSR107緩存規(guī)范
JSR 是 Java Specification Requests 的縮寫,意思是 Java 提案規(guī)范。當(dāng)然了這種實(shí)際使用中很少直接使用,但是其中概念是通用的:
javaCaching定義了五個(gè)核心接口:
- CacheProvider:管理控制多個(gè)CacheManager
- CacheManager:管理很多Cache
- Cache:類似Map的數(shù)據(jù)結(jié)構(gòu)并臨時(shí)存儲(chǔ)多個(gè)key-value對(duì)。
- Entry:記錄。就是一個(gè)k-v對(duì)。
- Expire:k-v對(duì)都有一個(gè)過期時(shí)間。

如果想使用這個(gè)緩存,要引入的依賴:
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
感興趣的可以自己去看看源碼,但是因?yàn)檫@個(gè)適配不太好,而且自身支持的緩存比較少。所以我們用這個(gè)并不多。所以這個(gè)就是簡單的介紹一下五個(gè)接口就過了。
Spring緩存抽象
Spring從3.1開始定義了它自己的Cache和CacheManager接口來統(tǒng)一不同的緩存技術(shù)。并支持使用JCache(JSP107)注解簡化開發(fā)。其中幾個(gè)重要的概念:
- CacheManager:緩存管理器,管理各種緩存(Cache)組件。
- Cache:緩存接口,定義緩存操作。其有多個(gè)實(shí)現(xiàn)。根據(jù)實(shí)現(xiàn)的不同可以執(zhí)行不同的緩存技術(shù)。比如:RedisCache。EhCacheCache,ConcurrentMapCache等。
- @Cacheable:主要針對(duì)方法配置,根據(jù)方法的請求參數(shù)對(duì)結(jié)果進(jìn)行緩存。(查詢方法上加入這個(gè)注解)
- @CachePut:保證方法被調(diào)用,又希望結(jié)果被緩存。(這個(gè)是方法總會(huì)調(diào)用,結(jié)果重新被緩存??梢岳斫鉃楦拢?/li>
- @CacheEvict:清空緩存。(刪除方法上添加這個(gè)注解)
- @EnableCaching:開啟基于注解的緩存。
- keyGenerator:緩存時(shí)key的生成策略
-
serialize:緩存時(shí)value序列化策略
截圖
下面讓我們簡單使用下緩存:
-
一個(gè)基本帶有數(shù)據(jù)庫的測試項(xiàng)目
springBoot項(xiàng)目,這幾個(gè)crud方法都是測通了的 - 想使用緩存注解,一定要在啟動(dòng)類上加@EnableCaching注解開啟這個(gè)功能
- 注解的使用:
- @Cacheable :將方法的運(yùn)行結(jié)果進(jìn)行緩存,以后再要相同的數(shù)據(jù)直接從緩存中讀取,不用調(diào)用方法了。
需要注意的是上面說了實(shí)際緩存是在Cache組件中。而Cache受管制與CacheManager。一個(gè)CacheManager管理多個(gè)Cache組件。所以每個(gè)Cache組件要有自己的名字。所以在每次存儲(chǔ)是要告訴緩存這個(gè)kv對(duì)是放在哪個(gè)組件中的。同時(shí)我們也可以以k-v對(duì)的形式放入(指定key),當(dāng)然了如果我們不指定key會(huì)默認(rèn)使用方法的參數(shù)的值。簡單說一下這個(gè)注解中的參數(shù):- cacheNames/value:指定緩存組件的名稱,數(shù)組的形式,可以指定多個(gè)cache名。
- key:緩存數(shù)據(jù)使用的key,可以指定,不指定默認(rèn)是方法參數(shù)的值。也可以用spEl表達(dá)式來寫,語法如下圖。
- keyGenerator:key的生成器,可以自己指定key的生成器的組件id(key和keyGenerator二選一使用)
- cacheManager/cacheResolver:指定緩存管理器/解析器。這兩個(gè)注解也是二選一的
- condition:符合指定條件的情況下才緩存。
- unless:否定緩存。當(dāng)unless指定的條件是false才保存??梢垣@取方法的結(jié)果判斷
-
sync:是否使用異步模式。默認(rèn)是false-同步。可以改為true變成異步的
這個(gè)注解的參數(shù)是支持spel表達(dá)式的,如下:
cache中spEL表達(dá)式
- @Cacheable :將方法的運(yùn)行結(jié)果進(jìn)行緩存,以后再要相同的數(shù)據(jù)直接從緩存中讀取,不用調(diào)用方法了。
說了那么多,我們在項(xiàng)目中測試一下,在一個(gè)查詢方法上加個(gè)緩存注解。這里用最簡版,只要指定cache不報(bào)錯(cuò)就行。如下頁面準(zhǔn)備:

當(dāng)我點(diǎn)擊接口訪問后,走了數(shù)據(jù)庫

重點(diǎn)是接下來,我再次查詢id是8的user信息:

至此可以確定,我們對(duì)查詢結(jié)果的緩存是成功了的。
既然我們緩存成功了,下一步就是對(duì)于緩存的原理的分析啦。畢竟知其然是表面,只能讓我們對(duì)付用,知其所以然才是本質(zhì),讓我們能用的更順手和調(diào)試更方便。
緩存實(shí)現(xiàn)的原理
首先cache也是自動(dòng)配置的,所以仍然要去自動(dòng)配置的包去找一下:

如圖我們看到這個(gè)cache中配置不多。但是除了一個(gè)autoConfiguration自動(dòng)配置類外,還有11個(gè)configuration這個(gè)是什么玩意兒?其實(shí)我們看名字是能猜出來大概是不同的緩存技術(shù)的配置類。隨便點(diǎn)進(jìn)去一個(gè)看看就能看到這些配置類上是有注解的,而且這些類有個(gè)共同點(diǎn):
@ConditionalOnMissingBean(CacheManager.class)
所有的類上都有這個(gè)注解,所以也就是說可以保證最終只有一個(gè)bean被注冊成功。


其實(shí)我們可以在配置文件中配置:
debug=true
打印一下哪些類生效了哪些沒生效。

因?yàn)橹耙呀?jīng)確定只有一個(gè)生效了,所以不用看別的了,別的那幾個(gè)xxcache都沒生效。
而這個(gè)類生效了的話,我們?nèi)ピ创a看看這個(gè)配置類干啥了:

其實(shí)這個(gè)源碼很容易看懂,就是向容器里注入了一個(gè)名稱是cacheManager的ConcurrentMapCacheManager類型的bean(因?yàn)闆]起名字所以默認(rèn)方法名。其實(shí)這個(gè)能看出來是父子類吧?)。
繼續(xù)說上面創(chuàng)建了一個(gè)ConcurrentMapCacheManager類型的bean。我們點(diǎn)進(jìn)這個(gè)類去看看這個(gè)類是什么?

其實(shí)ConcurrentMapCacheManager這個(gè)類還是能看懂的,里面有個(gè)線程安全的map。存的key是cache名(猜的),value是cache對(duì)象。這個(gè)我們因?yàn)橹熬驼f過這個(gè)了,所以我覺得很容易能猜出來。
然后方法大多數(shù)也都是本類的方法,仔細(xì)順著看也差不多能看懂,超出本類內(nèi)容的也就最后三個(gè)方法:

這里存取緩存的時(shí)候涉及到了一個(gè)新的類ConcurrentMapCache。其實(shí)看前綴就能猜的差不多,因?yàn)閏ache我們之前說了具體的存儲(chǔ)還是k-v對(duì)。這個(gè)是一個(gè)manager,管理多個(gè)cache。一個(gè)cache又管理多個(gè)kv對(duì)。著不就是套娃嘛!
其實(shí)這一塊代碼應(yīng)該還是比較順的,也沒那么復(fù)雜。剛剛分析了半天,下面我們可以用斷點(diǎn)的方式,親眼見證一下這個(gè)緩存代碼的走向:
代碼還是上面的代碼。我們在剛剛getCache這個(gè)方法上打個(gè)斷點(diǎn)。然后訪問接口。第一步就走到這個(gè)方法:

發(fā)現(xiàn)沒有這個(gè)名稱的cache,在確定沒有(這里用的雙重檢查鎖判空的)的時(shí)候創(chuàng)建一個(gè)叫做這個(gè)名字的cache并且里面沒有元素。

再把這個(gè)cache添加到管理器

下面順著代碼一步一步走,該進(jìn)方法就進(jìn),進(jìn)錯(cuò)了出來就完事了,反正是到了下一個(gè)關(guān)鍵點(diǎn):

回憶下上面說的不指定key默認(rèn)就是方法參數(shù)~~~

注意這個(gè)方法是ConcurrentMapCache中的,其實(shí)就是看這個(gè)key有沒有值。如果有值就直接返回了,沒值再去數(shù)據(jù)庫查詢。邏輯還是很簡單的。

總結(jié)一句話:@Cacheable標(biāo)注的方法執(zhí)行之前先檢查緩存中有沒有這個(gè)數(shù)據(jù),默認(rèn)按照參數(shù)的值作為key去查詢緩存,如果沒有就去運(yùn)行方法并將參數(shù)放入緩存中。如果有直接把結(jié)果返回。
至此,這個(gè)源碼算是跟著走一遍了。反正我看完是有點(diǎn)飄了,甚至覺得我自己都能寫一個(gè)緩存了,哈哈。開個(gè)小玩笑,這里我看到居然有人問服務(wù)器重啟緩存還在不在了。這里的緩存不使用任何中間件(比如redis,es之類的)我感覺其實(shí)和項(xiàng)目中自己創(chuàng)建了個(gè)map差不多,其實(shí)有很多時(shí)候我們也不知不覺自己實(shí)現(xiàn)了這種緩存功能呢。
上面簡單說了下原理,繼續(xù)說使用吧:
剛說了這個(gè)key默認(rèn)就是參數(shù)名。但是也可以自定義key的生成策略。寫也比較簡單,如下代碼:

自己實(shí)現(xiàn)的方式:

我這里只是做了個(gè)demo,大家也可以自己想實(shí)現(xiàn)策略的。然后把這個(gè)bean指定到之前的注解上:

然后重新運(yùn)行起來看一下key是什么:

咳咳,拼接的時(shí)候有點(diǎn)小失誤,拼出來的東西和我想得不太一樣。不過這個(gè)不重要。重要的是我們自定義key的生成策略確實(shí)起作用了。
下面說下一個(gè)注解:@CachePut
@CachePut:這個(gè)注解和上一個(gè)注解@Cacheable調(diào)用的時(shí)機(jī)不一樣。這個(gè)會(huì)先調(diào)用方法,然后把運(yùn)行的結(jié)果存到緩存中。
正常來講這個(gè)肯定是要和@Cacheable聯(lián)合使用的。一個(gè)正常的邏輯是查詢后將數(shù)據(jù)緩存起來。然后取的時(shí)候直接從緩存中取。但是如果這個(gè)對(duì)象發(fā)生改變了則要更新緩存。所以將這個(gè)對(duì)象對(duì)應(yīng)的key的修改方法上指定這個(gè)注解。有如下幾種方式:

@CacheEvict:刪除緩存。比如這條數(shù)據(jù)我們從數(shù)據(jù)庫中刪除,那么把這個(gè)緩存也刪除了得了。反正什么時(shí)候用開心就好,重點(diǎn)是這個(gè)用法,同上面兩個(gè)一樣,指定cache的名字,指定key 的名稱,就刪除了。

我這里反正是查詢一次有了緩存,再查尋發(fā)現(xiàn)不用存數(shù)據(jù)庫證明緩存確實(shí)生效了,然后調(diào)用del刪除這個(gè)id。再查詢發(fā)現(xiàn)又要走數(shù)據(jù)庫了。說明確實(shí)刪除成功!
當(dāng)然這個(gè)刪除緩存有一些特有的參數(shù):allEntries這個(gè)參數(shù)是所有的k-v對(duì)(指定的cache中)。意思是是否刪除所有的,默認(rèn)是false。如果是true的話則會(huì)清空這個(gè)cache的所有緩存。

還有一個(gè)截圖中最后那個(gè):beforeInvocation 是不是在方法執(zhí)行之前執(zhí)行。默認(rèn)是false。如果改成true會(huì)在方法執(zhí)行前執(zhí)行。
這個(gè)的用法是如果這個(gè)方法代碼出錯(cuò)了,那么就不會(huì)清除緩存了。但是如果在方法執(zhí)行前就不管方法執(zhí)行怎么樣都會(huì)清楚。
@Caching:中間可以指定多個(gè)cache注解。我覺得可以理解為組合指令吧。如下demo:

@CacheConfig:這個(gè)注解是寫在類上的,可以指定這個(gè)類中所有的注解都按照公共配置來配置。作用就是抽取緩存的公共配置。
緩存中間件
上面我們也試過了,默認(rèn)的化springBoot的緩存是simpleCache,但是正常我們工作中緩存都是放在了緩存中間件中的,所以這里說一下緩存中間件,常用的redis,ehcache,memcached等。這件說下用的比較多的redis。
關(guān)于redis的基本使用我之前已經(jīng)學(xué)完了,所以這里直接用了。
- 引入redis依賴
之前都說了spring-data幾乎都是有一個(gè)場景啟動(dòng)器的,所以我們只要引入這個(gè)場景啟動(dòng)器就行了,我是去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>
在項(xiàng)目中引入依賴后,我們一步到位一點(diǎn),直接看看這個(gè)redis自動(dòng)配置給我們干了什么事,因?yàn)樗械淖詣?dòng)配置格式都是XXXAutoConfiguration。所以直接找這個(gè)類:


簡單的看下就知道了這個(gè)類生效條件是有一個(gè)RedisOperations.class類,估計(jì)有這個(gè)類就證明引入了這個(gè)redis依賴。
然后里面有倆類,都是不存在才生效,也就意味著我們可以自己寫這個(gè)bean。一個(gè)是redisTemplate,一個(gè)是stringRedisTemplate。
其實(shí)這個(gè)XXXTempate在springboot中也是很常見的一個(gè)東西。就是模板嘛。一會(huì)兒我們可以試著用一下。
-
配置配置文件
項(xiàng)目中想使用redis。還是要做一些基本的配置的。比如redis的地址,端口,用戶名,密碼之類的。關(guān)于redis可以配置什么,怎么配置也可以自己去配置文件中找一下,如下截圖:
redis配置內(nèi)容
就像圖中的屬性我們都可以配置,不過大家可以看到有一些默認(rèn)配置:比如端口6379,還有地址是localhost。又因?yàn)槲业膔edis是最基本的redis,這些默認(rèn)配置都o(jì)k,還沒有密碼。。所以理論上什么都不配置就可以使用。。。所以我這里直接用了。。
-
使用redisTemplate/stringRedisTemplate
第二個(gè)很明顯就是操作字符串的。第一個(gè)是所有類型的。我們只要直接注入就可以使用了。如下代碼:
注入redisTemplate
這里有很重要的一點(diǎn)?。。∥覀冊谥白⑷隻ean的時(shí)候看到了注入的是一個(gè)兩個(gè)泛型都是Object的RedisTemplate<Object, Object>。所以這里引用也要這么用,如果泛型不對(duì)就會(huì)報(bào)錯(cuò)說找不到這個(gè)bean的。當(dāng)然了也可以象我這樣省略不寫。雖然會(huì)有警告。。
這個(gè)代碼的運(yùn)行結(jié)果:
按照預(yù)期的實(shí)現(xiàn)了
事實(shí)證明這個(gè)redis的操作就這么完成了。再看下我們都做了什么:
- 導(dǎo)包
- 因?yàn)榕渲枚际腔镜?,所以沒寫配置
- 測試的時(shí)候直接注入使用了
簡單來說就是導(dǎo)包即用,這個(gè)簡直方便的嚇人。。這里只測試了字符串操作,其實(shí)也可以試試別的數(shù)據(jù)格式。這里因?yàn)橛弥闊┧晕揖筒徽f了。但是因?yàn)槲疑厦嫱祽杏玫淖址0逅杂袀€(gè)問題沒暴露,這個(gè)說下這個(gè)問題:

問題如下,獲取和存儲(chǔ)正常,但是存進(jìn)redis中的東西莫名其妙多了點(diǎn)啥,雖然不影響使用但是也看著好不順眼。而且這個(gè)這個(gè)前綴第一反應(yīng)應(yīng)該就是亂碼,但是這個(gè)因?yàn)楹竺娴膬?nèi)容是對(duì)的,所以應(yīng)該不是亂碼,第二個(gè)會(huì)讓其變得這么奇怪的可能也就序列化了。
點(diǎn)進(jìn)這個(gè)redisTemplate類就會(huì)發(fā)現(xiàn)一進(jìn)去就能看到個(gè)默認(rèn)序列化的屬性,如下圖:

這里因?yàn)橐粡垐D截不下來所以說下默認(rèn)的是用的jdk的序列化:
if (defaultSerializer == null) {
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
雖然名頭挺大,jdk自帶的,但是我們平時(shí)用json足以說明它自帶的不好用啊,所以這里我們可以自己注入bean,并且設(shè)置為我們想要的設(shè)置。如下代碼:

大家記得當(dāng)時(shí)系統(tǒng)自動(dòng)注入的那個(gè)redisTemplate是不存在才會(huì)注入,我自己寫了以后會(huì)注入我這個(gè)不會(huì)注入之前那個(gè)了。代碼是我完全copy之前那個(gè)的。就加了一句話,指定這個(gè)序列化的方式了。然后運(yùn)行:

雖然還有點(diǎn)小問題,但是怎么也比之前的那個(gè)好了。。。而且也說明之前那個(gè)類似亂碼的東西就是序列化的鍋。至于這個(gè)雙雙引號(hào)。。??赡苁俏野炎址俅涡蛄谢脑颍课覒岩蛇@個(gè)還是序列化方式的問題,不過我這個(gè)項(xiàng)目沒引入gson或者fastjson甚至libjson之類的。。。先不管了。
畢竟說redis的主旨也不是為了簡單使用的,回歸正題,說下緩存。
之前什么都沒配置,默認(rèn)緩存用的simpleCache?,F(xiàn)在我們引入了redis,根據(jù)之前的分析滿足了redisCacheConfiguration的條件了,所以應(yīng)該會(huì)引入redisCache了。而且因?yàn)閏acheManager非空才生效的約束,所以應(yīng)該只有這個(gè)redis的會(huì)生效。我們可以依然用debug=true的方式看一眼,我就不看了。
下面我們繼續(xù)使用緩存,看看實(shí)際上的內(nèi)容是不是按照我們的猜測存到了redis中,下面的測試代碼:

如上測試代碼,我們隨便傳幾個(gè)名字測試下:
首先我們根據(jù)控制臺(tái)打印確定了除了一次查詢的名字剩下都不走入方法了,所以證明緩存生效了。這個(gè)時(shí)候我們看redis:

事實(shí)證明確實(shí)是存入到redis中了,而且這個(gè)cache名字也拼到了key上。名稱-參數(shù)的方式來存儲(chǔ)的。挺有意思的東西。
其實(shí)這里我為了偷懶又用了stringRedisTemplate了。因?yàn)橛媚莻€(gè)又要因?yàn)樾蛄谢艹螅凑@里自己改序列化方式就行了。
至此,這個(gè)簡單的緩存的使用和原理就說完了,這里除了自帶的緩存,就介紹了一些基本的概念,原理,只用redis做了一個(gè)舉例。但是話說回來如果知道了原理想要使用也簡單的很。大概的流程就是如果使用spring boot整合過可以自動(dòng)注入的東西,就先去看xxxProperties,看看有什么屬性方便自己配置,第二步看看自動(dòng)配置類中bean在什么情況下使用。最后個(gè)性化配置使用就完了。這一套東西是一個(gè)教人學(xué)習(xí)的流程。當(dāng)接入redis時(shí)這樣,當(dāng)接入mq也是這樣,同理別的都是。授人與魚不如授人與漁。這個(gè)遠(yuǎn)比單純的教會(huì)api調(diào)用有用的多。
本篇筆記就記到這里,如果稍微幫到你了記得點(diǎn)個(gè)喜歡點(diǎn)個(gè)關(guān)注,也祝大家工作順順利利!另外最近有點(diǎn)迷茫,甚至到了不知道學(xué)習(xí)是為了什么的地步。感覺現(xiàn)在之所以堅(jiān)持每天刷題,看教學(xué)視頻,做筆記已經(jīng)變成了習(xí)慣。我不知道每天堅(jiān)持學(xué)習(xí)有什么意義。但是卻知道什么都不做更沒可能。與大家共勉一句話吧:除非付出行動(dòng),否則口說無憑,沒有人能通過祈禱試圖改變?nèi)松?/strong>





