緩存使用指南
緩存是現(xiàn)在系統(tǒng)中必不可少的模塊,并且已經(jīng)成為了高并發(fā)高性能架構(gòu)的一個(gè)關(guān)鍵組件。這篇博客我們來(lái)分析一下使用緩存的正確姿勢(shì)。
緩存能解決的問(wèn)題
提升性能
- 絕大多數(shù)情況下,select是出現(xiàn)性能問(wèn)題最大的地方。一方面,select 會(huì)有很多像 join、group、order、like等這樣豐富的語(yǔ)義,而這些語(yǔ)義是非常耗性能的;另一方面,大多數(shù)應(yīng)用都是讀多寫(xiě)少,所以加劇了慢查詢的問(wèn)題。
- 分布式系統(tǒng)中遠(yuǎn)程調(diào)用也會(huì)耗很多性能,因?yàn)橛芯W(wǎng)絡(luò)開(kāi)銷(xiāo),會(huì)導(dǎo)致整體的響應(yīng)時(shí)間下降。為了挽救這樣的性能開(kāi)銷(xiāo),在業(yè)務(wù)允許的情況(不需要太實(shí)時(shí)的數(shù)據(jù))下,使用緩存是非常必要的事情。
緩解數(shù)據(jù)庫(kù)壓力
- 當(dāng)用戶請(qǐng)求增多時(shí),數(shù)據(jù)庫(kù)的壓力將大大增加,通過(guò)緩存能夠大大降低數(shù)據(jù)庫(kù)的壓力。
緩存的適用場(chǎng)景
對(duì)于數(shù)據(jù)實(shí)時(shí)性要求不高
- 對(duì)于一些經(jīng)常訪問(wèn)但是很少改變的數(shù)據(jù),讀明顯多于寫(xiě),適用緩存就很有必要。比如一些網(wǎng)站配置項(xiàng)。
對(duì)于性能要求高
- 比如一些秒殺活動(dòng)場(chǎng)景。
緩存三種模式
一般來(lái)說(shuō),緩存有以下三種模式:
- Cache Aside 更新模式
- Read/Write Through 更新模式
- Write Behind Caching 更新模式
通俗一點(diǎn)來(lái)講就是,同時(shí)更新緩存和數(shù)據(jù)庫(kù)(Cache Aside 更新模式);先更新緩存,緩存負(fù)責(zé)同步更新數(shù)據(jù)庫(kù)(Read/Write Through 更新模式);先更新緩存,緩存定時(shí)異步更新數(shù)據(jù)庫(kù)(Write Behind Caching 更新模式)。這三種模式各有優(yōu)劣,可以根據(jù)業(yè)務(wù)場(chǎng)景選擇使用。
Cache Aside 更新模式
這是最常用的緩存模式了,具體的流程是:
- 失效:應(yīng)用程序先從 cache 取數(shù)據(jù),沒(méi)有得到,則從數(shù)據(jù)庫(kù)中取數(shù)據(jù),成功后,放到緩存中。
- 命中:應(yīng)用程序從 cache 中取數(shù)據(jù),取到后返回。
-
更新:先把數(shù)據(jù)存到數(shù)據(jù)庫(kù)中,成功后,再讓緩存失效。
注意我們上面所提到的,緩存更新時(shí)先更新數(shù)據(jù)庫(kù),然后在讓緩存失效。那么為什么不是直接更新緩存呢?這里有一些緩存更新的坑,我們需要避免入坑。
避坑指南一
先更新數(shù)據(jù)庫(kù),再更新緩存。這種做法最大的問(wèn)題就是兩個(gè)并發(fā)的寫(xiě)操作導(dǎo)致臟數(shù)據(jù)。如下圖(以Redis和Mysql為例),兩個(gè)并發(fā)更新操作,數(shù)據(jù)庫(kù)先更新的反而后更新緩存,數(shù)據(jù)庫(kù)后更新的反而先更新緩存。這樣就會(huì)造成數(shù)據(jù)庫(kù)和緩存中的數(shù)據(jù)不一致,應(yīng)用程序中讀取的都是臟數(shù)據(jù)。

避坑指南二
先刪除緩存,再更新數(shù)據(jù)庫(kù)。這個(gè)邏輯是錯(cuò)誤的,因?yàn)閮蓚€(gè)并發(fā)的讀和寫(xiě)操作導(dǎo)致臟數(shù)據(jù)。如下圖(以Redis和Mysql為例)。假設(shè)更新操作先刪除了緩存,此時(shí)正好有一個(gè)并發(fā)的讀操作,沒(méi)有命中緩存后從數(shù)據(jù)庫(kù)中取出老數(shù)據(jù)并且更新回緩存,這個(gè)時(shí)候更新操作也完成了數(shù)據(jù)庫(kù)更新。此時(shí),數(shù)據(jù)庫(kù)和緩存中的數(shù)據(jù)不一致,應(yīng)用程序中讀取的都是原來(lái)的數(shù)據(jù)(臟數(shù)據(jù))。

避坑指南三
先更新數(shù)據(jù)庫(kù),再刪除緩存。這種做法其實(shí)不能算是坑,在實(shí)際的系統(tǒng)中也推薦使用這種方式。但是這種方式理論上還是可能存在問(wèn)題。如下圖(以Redis和Mysql為例),查詢操作沒(méi)有命中緩存,然后查詢出數(shù)據(jù)庫(kù)的老數(shù)據(jù)。此時(shí)有一個(gè)并發(fā)的更新操作,更新操作在讀操作之后更新了數(shù)據(jù)庫(kù)中的數(shù)據(jù)并且刪除了緩存中的數(shù)據(jù)。然而讀操作將從數(shù)據(jù)庫(kù)中讀取出的老數(shù)據(jù)更新回了緩存。這樣就會(huì)造成數(shù)據(jù)庫(kù)和緩存中的數(shù)據(jù)不一致,應(yīng)用程序中讀取的都是原來(lái)的數(shù)據(jù)(臟數(shù)據(jù))。

但是,仔細(xì)想一想,這種并發(fā)的概率極低。因?yàn)檫@個(gè)條件需要發(fā)生在讀緩存時(shí)緩存失效,而且有一個(gè)并發(fā)的寫(xiě)操作。實(shí)際上數(shù)據(jù)庫(kù)的寫(xiě)操作會(huì)比讀操作慢得多,而且還要加鎖,而讀操作必需在寫(xiě)操作前進(jìn)入數(shù)據(jù)庫(kù)操作,又要晚于寫(xiě)操作更新緩存,所有這些條件都具備的概率并不大。但是為了避免這種極端情況造成臟數(shù)據(jù)所產(chǎn)生的影響,我們還是要為緩存設(shè)置過(guò)期時(shí)間。
Read/Write Through 更新模式
在上面的 Cache Aside 更新模式中,應(yīng)用代碼需要維護(hù)兩個(gè)數(shù)據(jù)存儲(chǔ),一個(gè)是緩存(Cache),一個(gè)是數(shù)據(jù)庫(kù)(Repository)。而在Read/Write Through 更新模式中,應(yīng)用程序只需要維護(hù)緩存,數(shù)據(jù)庫(kù)的維護(hù)工作由緩存代理了。

Read Through
Read Through 模式就是在查詢操作中更新緩存,也就是說(shuō),當(dāng)緩存失效的時(shí)候,Cache Aside 模式是由調(diào)用方負(fù)責(zé)把數(shù)據(jù)加載入緩存,而 Read Through 則用緩存服務(wù)自己來(lái)加載。
Write Through
Write Through 模式和 Read Through 相仿,不過(guò)是在更新數(shù)據(jù)時(shí)發(fā)生。當(dāng)有數(shù)據(jù)更新的時(shí)候,如果沒(méi)有命中緩存,直接更新數(shù)據(jù)庫(kù),然后返回。如果命中了緩存,則更新緩存,然后由緩存自己更新數(shù)據(jù)庫(kù)(這是一個(gè)同步操作)。
Write Behind Caching 更新模式
Write Behind Caching 更新模式就是在更新數(shù)據(jù)的時(shí)候,只更新緩存,不更新數(shù)據(jù)庫(kù),而我們的緩存會(huì)異步地批量更新數(shù)據(jù)庫(kù)。這個(gè)設(shè)計(jì)的好處就是直接操作內(nèi)存速度快。因?yàn)楫惒?,Write Behind Caching 更新模式還可以合并對(duì)同一個(gè)數(shù)據(jù)的多次操作到數(shù)據(jù)庫(kù),所以性能的提高是相當(dāng)可觀的。
但其帶來(lái)的問(wèn)題是,數(shù)據(jù)不是強(qiáng)一致性的,而且可能會(huì)丟失。另外,Write Behind Caching 更新模式實(shí)現(xiàn)邏輯比較復(fù)雜,因?yàn)樗枰_認(rèn)有哪些數(shù)據(jù)是被更新了的,哪些數(shù)據(jù)需要刷到持久層上。只有在緩存需要失效的時(shí)候,才會(huì)把它真正持久起來(lái)。

總結(jié)三種緩存模式的優(yōu)缺點(diǎn):
- Cache Aside 更新模式實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,但是需要維護(hù)兩個(gè)數(shù)據(jù)存儲(chǔ),一個(gè)是緩存(Cache),一個(gè)是數(shù)據(jù)庫(kù)(Repository)。
- Read/Write Through 更新模式只需要維護(hù)一個(gè)數(shù)據(jù)存儲(chǔ)(緩存),但是實(shí)現(xiàn)起來(lái)要復(fù)雜一些。
- Write Behind Caching 更新模式和Read/Write Through 更新模式類(lèi)似,區(qū)別是Write Behind Caching 更新模式的數(shù)據(jù)持久化操作是異步的,但是Read/Write Through 更新模式的數(shù)據(jù)持久化操作是同步的。優(yōu)點(diǎn)是直接操作內(nèi)存速度快,多次操作可以合并持久化到數(shù)據(jù)庫(kù)。缺點(diǎn)是數(shù)據(jù)可能會(huì)丟失,例如系統(tǒng)斷電等。
緩存是通過(guò)犧牲強(qiáng)一致性來(lái)提高性能的。所以使用緩存提升性能,就是會(huì)有數(shù)據(jù)更新的延遲。這需要我們?cè)谠O(shè)計(jì)時(shí)結(jié)合業(yè)務(wù)仔細(xì)思考是否適合用緩存。然后緩存一定要設(shè)置過(guò)期時(shí)間,這個(gè)時(shí)間太短太長(zhǎng)都不好,太短的話請(qǐng)求可能會(huì)比較多的落到數(shù)據(jù)庫(kù)上,這也意味著失去了緩存的優(yōu)勢(shì)。太長(zhǎng)的話緩存中的臟數(shù)據(jù)會(huì)使系統(tǒng)長(zhǎng)時(shí)間處于一個(gè)延遲的狀態(tài),而且系統(tǒng)中長(zhǎng)時(shí)間沒(méi)有人訪問(wèn)的數(shù)據(jù)一直存在內(nèi)存中不過(guò)期,浪費(fèi)內(nèi)存。
Redis簡(jiǎn)介
Redis是當(dāng)前比較熱門(mén)的NOSQL系統(tǒng)之一,它是一個(gè)開(kāi)源的使用ANSI c語(yǔ)言編寫(xiě)的key-value存儲(chǔ)系統(tǒng)(區(qū)別于MySQL的二維表格的形式存儲(chǔ)。)。和Memcache類(lèi)似,但很大程度補(bǔ)償了Memcache的不足。和Memcache一樣,Redis數(shù)據(jù)都是緩存在計(jì)算機(jī)內(nèi)存中,不同的是,Memcache只能將數(shù)據(jù)緩存到內(nèi)存中,無(wú)法自動(dòng)定期寫(xiě)入硬盤(pán),這就表示,一斷電或重啟,內(nèi)存清空,數(shù)據(jù)丟失。所以Memcache的應(yīng)用場(chǎng)景適用于緩存無(wú)需持久化的數(shù)據(jù)。而Redis不同的是它會(huì)周期性的把更新的數(shù)據(jù)寫(xiě)入磁盤(pán)或者把修改操作寫(xiě)入追加的記錄文件,實(shí)現(xiàn)數(shù)據(jù)的持久化。
它支持多種類(lèi)型的數(shù)據(jù)結(jié)構(gòu),如字符串(Strings),散列(Hash),列表(List),集合(Set),有序集合(Sorted Set或者是ZSet)與范圍查詢,Bitmaps,Hyperloglogs 和地理空間(Geospatial)索引半徑查詢。其中常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)類(lèi)型有:String、List、Set、Hash、ZSet這5種。
Redis 內(nèi)置了復(fù)制(Replication),LUA腳本(Lua scripting), LRU驅(qū)動(dòng)事件(LRU eviction),事務(wù)(Transactions) 和不同級(jí)別的磁盤(pán)持久化(Persistence),并通過(guò) Redis哨兵(Sentinel)和自動(dòng)分區(qū)(Cluster)提供高可用性(High Availability)。
Redis也提供了持久化的選項(xiàng),這些選項(xiàng)可以讓用戶將自己的數(shù)據(jù)保存到磁盤(pán)上面進(jìn)行存儲(chǔ)。根據(jù)實(shí)際情況,可以每隔一定時(shí)間將數(shù)據(jù)集導(dǎo)出到磁盤(pán)(快照),或者追加到命令日志中(AOF只追加文件),他會(huì)在執(zhí)行寫(xiě)命令時(shí),將被執(zhí)行的寫(xiě)命令復(fù)制到硬盤(pán)里面。您也可以關(guān)閉持久化功能,將Redis作為一個(gè)高效的網(wǎng)絡(luò)的緩存數(shù)據(jù)功能使用。
Redis不使用表,他的數(shù)據(jù)庫(kù)不會(huì)預(yù)定義或者強(qiáng)制去要求用戶對(duì)Redis存儲(chǔ)的不同數(shù)據(jù)進(jìn)行關(guān)聯(lián)。
數(shù)據(jù)庫(kù)的工作模式按存儲(chǔ)方式可分為:硬盤(pán)數(shù)據(jù)庫(kù)和內(nèi)存數(shù)據(jù)庫(kù)。Redis 將數(shù)據(jù)儲(chǔ)存在內(nèi)存里面,讀寫(xiě)數(shù)據(jù)的時(shí)候都不會(huì)受到硬盤(pán) I/O 速度的限制,所以速度極快。
**硬盤(pán)數(shù)據(jù)庫(kù)的工作模式: **
**內(nèi)存數(shù)據(jù)庫(kù)的工作模式: **
Redis到底有多快
Redis采用的是基于內(nèi)存的采用的是單進(jìn)程單線程模型的 KV 數(shù)據(jù)庫(kù),由C語(yǔ)言編寫(xiě),官方提供的數(shù)據(jù)是可以達(dá)到100000+的QPS(每秒內(nèi)查詢次數(shù))。這個(gè)數(shù)據(jù)不比采用單進(jìn)程多線程的同樣基于內(nèi)存的 KV 數(shù)據(jù)庫(kù) Memcached 差!有興趣的可以參考官方的基準(zhǔn)程序測(cè)試《How fast is Redis?》(https://redis.io/topics/benchmarks)
橫軸是連接數(shù),縱軸是QPS。此時(shí),這張圖反映了一個(gè)數(shù)量級(jí),希望大家在面試的時(shí)候可以正確的描述出來(lái),不要問(wèn)你的時(shí)候,你回答的數(shù)量級(jí)相差甚遠(yuǎn)!
Redis為什么這么快
- 1、完全基于內(nèi)存,絕大部分請(qǐng)求是純粹的內(nèi)存操作,非??焖?。數(shù)據(jù)存在內(nèi)存中,類(lèi)似于HashMap,HashMap的優(yōu)勢(shì)就是查找和操作的時(shí)間復(fù)雜度都是O(1);
- 2、數(shù)據(jù)結(jié)構(gòu)簡(jiǎn)單,對(duì)數(shù)據(jù)操作也簡(jiǎn)單,Redis中的數(shù)據(jù)結(jié)構(gòu)是專(zhuān)門(mén)進(jìn)行設(shè)計(jì)的;
- 3、采用單線程,避免了不必要的上下文切換和競(jìng)爭(zhēng)條件,也不存在多進(jìn)程或者多線程導(dǎo)致的切換而消耗 CPU,不用去考慮各種鎖的問(wèn)題,不存在加鎖釋放鎖操作,沒(méi)有因?yàn)榭赡艹霈F(xiàn)死鎖而導(dǎo)致的性能消耗;
- 4、使用多路I/O復(fù)用模型,非阻塞IO;
- 5、使用底層模型不同,它們之間底層實(shí)現(xiàn)方式以及與客戶端之間通信的應(yīng)用協(xié)議不一樣,Redis直接自己構(gòu)建了VM 機(jī)制 ,因?yàn)橐话愕南到y(tǒng)調(diào)用系統(tǒng)函數(shù)的話,會(huì)浪費(fèi)一定的時(shí)間去移動(dòng)和請(qǐng)求;
以上幾點(diǎn)都比較好理解,下邊我們針對(duì)多路 I/O 復(fù)用模型進(jìn)行簡(jiǎn)單的探討:
多路 I/O 復(fù)用模型
多路I/O復(fù)用模型是利用 select、poll、epoll 可以同時(shí)監(jiān)察多個(gè)流的 I/O 事件的能力,在空閑的時(shí)候,會(huì)把當(dāng)前線程阻塞掉,當(dāng)有一個(gè)或多個(gè)流有 I/O 事件時(shí),就從阻塞態(tài)中喚醒,于是程序就會(huì)輪詢一遍所有的流(epoll 是只輪詢那些真正發(fā)出了事件的流),并且只依次順序的處理就緒的流,這種做法就避免了大量的無(wú)用操作。
這里“多路”指的是多個(gè)網(wǎng)絡(luò)連接,“復(fù)用”指的是復(fù)用同一個(gè)線程。采用多路 I/O 復(fù)用技術(shù)可以讓單個(gè)線程高效的處理多個(gè)連接請(qǐng)求(盡量減少網(wǎng)絡(luò) IO 的時(shí)間消耗),且 Redis 在內(nèi)存中操作數(shù)據(jù)的速度非??欤簿褪钦f(shuō)內(nèi)存內(nèi)的操作不會(huì)成為影響Redis性能的瓶頸,主要由以上幾點(diǎn)造就了 Redis 具有很高的吞吐量。
那么為什么Redis是單線程的
我們首先要明白,上邊的種種分析,都是為了營(yíng)造一個(gè)Redis很快的氛圍!官方FAQ表示,因?yàn)镽edis是基于內(nèi)存的操作,CPU不是Redis的瓶頸,Redis的瓶頸最有可能是機(jī)器內(nèi)存的大小或者網(wǎng)絡(luò)帶寬。既然單線程容易實(shí)現(xiàn),而且CPU不會(huì)成為瓶頸,那就順理成章地采用單線程的方案了(畢竟采用多線程會(huì)有很多麻煩!)。

注意:
這里我們一直在強(qiáng)調(diào)的單線程,只是在處理我們的網(wǎng)絡(luò)請(qǐng)求的時(shí)候只有一個(gè)線程來(lái)處理,一個(gè)正式的Redis Server運(yùn)行的時(shí)候肯定是不止一個(gè)線程的,這里需要大家明確的注意一下!Redis 在持久化時(shí)會(huì)調(diào)用 glibc 的函數(shù)fork產(chǎn)生一個(gè)子進(jìn)程,快照持久化完全交給子進(jìn)程來(lái)處理,父進(jìn)程繼續(xù)處理客戶端請(qǐng)求。子進(jìn)程剛剛產(chǎn)生時(shí),它和父進(jìn)程共享內(nèi)存里面的代碼段和數(shù)據(jù)段。這時(shí)你可以將父子進(jìn)程想像成一個(gè)連體嬰兒,共享身體。這是 Linux 操作系統(tǒng)的機(jī)制,為了節(jié)約內(nèi)存資源,所以盡可能讓它們共享起來(lái)。在進(jìn)程分離的一瞬間,內(nèi)存的增長(zhǎng)幾乎沒(méi)有明顯變化。
Redis的特點(diǎn)
- Redis讀取的速度是110000次/s,寫(xiě)的速度是81000次/s
- 原子 。Redis的所有操作都是原子性的,同時(shí)Redis還支持對(duì)幾個(gè)操作全并后的原子性執(zhí)行。
- 支持多種數(shù)據(jù)結(jié)構(gòu):string(字符串);list(列表);hash(哈希),set(集合);zset(有序集合)
- 持久化,主從復(fù)制(集群)
- 支持過(guò)期時(shí)間,支持事務(wù),消息訂閱。
- 官方不支持window,但是有第三方版本。
SpringBoot整合Redis
- 1、引入maven
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 高版本redis的lettuce需要commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
<!-- MyBatis 逆向工程 插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<!--允許移動(dòng)生成的文件 -->
<verbose>true</verbose>
<!-- 是否覆蓋 -->
<overwrite>true</overwrite>
<!-- 自動(dòng)生成的配置 -->
<configurationFile>
${basedir}/src/main/resources/generator/generatorConfig.xml
</configurationFile>
</configuration>
<!--下面這兩個(gè)可以不配置-->
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.6</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
- 2、新建RedisConfig作為配置類(lèi)
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
- 3、新建RedisUtil作為Redis工具類(lèi)
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
public boolean set(String key,Object value){
try{
redisTemplate.opsForValue().set(key,value);
return true;
}catch (Exception e){
return false;
}
}
public boolean set(String key,Object value,long time){
try{
redisTemplate.opsForValue().set(key,value,time, TimeUnit.SECONDS);
return true;
}catch (Exception e){
return false;
}
}
public Object get(String key){
return key == null ? null : redisTemplate.opsForValue().get(key);
}
......
}
- 4、application.properties加入Redis相關(guān)配置
# redis
# Redis服務(wù)器地址
spring.redis.host=127.0.0.1
# Redis服務(wù)器連接端口
spring.redis.port=6379
# Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0)
spring.redis.database=0
# Redis服務(wù)器連接密碼(默認(rèn)為空)
spring.redis.password=
# 連接超時(shí)時(shí)間(毫秒)
spring.redis.timeout=10000
# 以下連接池已在SpringBoot2.0不推薦使用
#spring.redis.pool.max-active=8
#spring.redis.pool.max-wait=-1
#spring.redis.pool.max-idle=8
#spring.redis.pool.min-idle=0
# Jedis
#spring.redis.jredis.max-active=8
#spring.redis.jredis.max-wait=10000
#spring.redis.jredis.max-idle=8
#spring.redis.jredis.min-idle=0
# Lettuce
# 連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制)
spring.redis.lettuce.pool.max-active=8
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)
spring.redis.lettuce.pool.max-wait=10000
# 連接池中的最大空閑連接
spring.redis.lettuce.pool.max-idle=8
# 連接池中的最小空閑連接
spring.redis.lettuce.pool.min-idle=0
# 關(guān)閉超時(shí)時(shí)間
spring.redis.lettuce.shutdown-timeout=100
- 5、測(cè)試redis
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {
@Autowired
private RedisUtil redisUtil;
@Test
public void test() throws InterruptedException {
redisUtil.set("yibo","你好",3);
System.out.println(redisUtil.get("yibo"));
Thread.sleep(3000);
System.out.println(redisUtil.get("yibo"));
}
}
企業(yè)級(jí)緩存使用
- 定義倉(cāng)儲(chǔ)層接口PersonRepository,采用數(shù)據(jù)庫(kù)和緩存實(shí)現(xiàn)
public interface PersonRepository {
Person getPersonById(Integer id);
}
@Component
public class PersonRepositoryImpl implements PersonRepository {
@Autowired
private PersonMapper personMapper;
public Person getPersonById(Integer id){
return personMapper.selectByPrimaryKey(id);
}
}
@Component
@Slf4j
public class PersonRepositoryCacheImpl implements PersonRepository {
private static final String CACHE_PREFIX="cache_prefix_";
@Autowired
private RedisUtil redisUtil;
@Resource(name="personRepositoryImpl")
private PersonRepository personRepository;
//這個(gè)用作緩存穿透使用
private Person nullPerson = new Person(-1);
//用隨機(jī)數(shù)防止緩存雪崩
private static final Random random = new Random();
@Override
public Person getPersonById(Integer id) {
Person person = getPersonFromCache(id);
if(person == null){
log.info("cache not hit");
person = personRepository.getPersonById(id);
cachePerson(id,person);
}else if(-1 == person.getId()){
log.warn("cache hit null");
return null;
}
log.info("cache hit id");
return person;
}
private void cachePerson(Integer id,Person person){
if(person != null){
redisUtil.set(generateCacheKey(id),JSON.toJSONString(person),random.nextInt(10)+5);
}else {
redisUtil.set(generateCacheKey(id),JSON.toJSONString(nullPerson),random.nextInt(10)+5);
}
}
private Person getPersonFromCache(Integer id){
String person = (String)redisUtil.get(generateCacheKey(id));
if(StringUtils.isEmpty(person)){
return null;
}
return JSON.parseObject(person,Person.class);
}
private String generateCacheKey(Integer id){
return CACHE_PREFIX + id;
}
}
- 定義服務(wù)接口PersonService,實(shí)現(xiàn)服務(wù)接口api并調(diào)用倉(cāng)儲(chǔ)層
public interface PersonService {
Person getPersonById(Integer id);
}
@Service
public class PersonServiceImpl implements PersonService {
@Resource(name="personRepositoryCacheImpl")
private PersonRepository personRepository;
@Override
public Person getPersonById(Integer id) {
return personRepository.getPersonById(id);
}
}
緩存擊穿
- 原因:一個(gè)key非常熱點(diǎn),在不停的扛著大并發(fā),大并發(fā)集中對(duì)這一個(gè)點(diǎn)進(jìn)行訪問(wèn),當(dāng)這個(gè)key在失效的瞬間,持續(xù)的大并發(fā)就穿破緩存,直接請(qǐng)求數(shù)據(jù)庫(kù),導(dǎo)致DB瞬間壓力過(guò)大,壓垮DB,就像在一個(gè)屏障上鑿開(kāi)了一個(gè)洞。
- 方案:
- 1、熱點(diǎn)數(shù)據(jù)緩存永不過(guò)期
- 2、使用分布式鎖,保證同一時(shí)刻只能有一個(gè)查詢請(qǐng)求重新加載熱點(diǎn)數(shù)據(jù)到緩存中
- 3、設(shè)置邏輯過(guò)期時(shí)間
- 緩存中的熱點(diǎn)數(shù)據(jù)中冗余一個(gè)邏輯過(guò)期時(shí)間,但數(shù)據(jù)在Redis不設(shè)置過(guò)期時(shí)間
當(dāng)一個(gè)請(qǐng)求拿到Redis中的數(shù)據(jù)時(shí),判斷邏輯過(guò)期時(shí)間是否到期,如果沒(méi)有到期,直接返回,如果到期則開(kāi)啟另一個(gè)線程獲得鎖后去查詢數(shù)據(jù)庫(kù)并將查詢的最新數(shù)據(jù)寫(xiě)回Redis,而當(dāng)前請(qǐng)求返回已經(jīng)查詢的數(shù)據(jù)。
- 緩存中的熱點(diǎn)數(shù)據(jù)中冗余一個(gè)邏輯過(guò)期時(shí)間,但數(shù)據(jù)在Redis不設(shè)置過(guò)期時(shí)間
緩存穿透
- 原因:惡意攻擊去查數(shù)據(jù)庫(kù)一定不存在的數(shù)據(jù),對(duì)數(shù)據(jù)庫(kù)造成壓力,甚至壓垮數(shù)據(jù)庫(kù)
- 方案:
- 1、使用特殊緩存空值標(biāo)識(shí)對(duì)象不存在
- 2、使用布隆過(guò)濾器
緩存雪崩
- 原因:在某一個(gè)時(shí)間段,緩存集中過(guò)期失效,對(duì)于數(shù)據(jù)庫(kù)而言,就會(huì)產(chǎn)生周期性的壓力波峰
- 方案:
- 1、針對(duì)大量緩存同時(shí)過(guò)期,設(shè)置緩存過(guò)期時(shí)間加上隨機(jī)因子,盡可能分散緩存過(guò)期時(shí)間
- 2、而針對(duì)Redis宕機(jī)導(dǎo)致的緩存雪崩,可以提前搭建好Redis的主從服務(wù)器進(jìn)行數(shù)據(jù)同步,并配置哨兵機(jī)制,這樣在Redis服務(wù)器因?yàn)殄礄C(jī)而無(wú)法提供服務(wù)時(shí),可以由哨兵將Redis從服務(wù)器設(shè)置為主服務(wù)器,繼續(xù)提供服務(wù)。
參考:
https://www.cnblogs.com/songwenjie/p/9027012.html
https://www.cnblogs.com/taiyonghai/p/9454764.html
https://www.cnblogs.com/jpfss/p/11016445.html
https://www.cnblogs.com/xxj-bigshow/p/10314414.html
