緩存這匹“野馬”,你駕馭得了嗎?
原創(chuàng)在之前的文章《你應(yīng)該知道的緩存進(jìn)化史》中介紹了愛(ài)奇藝的緩存架構(gòu)和緩存的進(jìn)化歷史。
【51CTO.com原創(chuàng)稿件】俗話說(shuō)得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好這些工具,本篇將分為如下幾個(gè)方面介紹如何利用好緩存:
- 你真的需要緩存嗎
- 如何選擇合適的緩存
- 多級(jí)緩存
- 緩存更新
- 緩存挖坑三劍客
- 緩存污染
- 序列化
- GC調(diào)優(yōu)
- 緩存的監(jiān)控
- 一款好的框架
- 總結(jié)
你真的需要緩存嗎
在使用緩存之前,需要確認(rèn)你的項(xiàng)目是否真的需要緩存。使用緩存會(huì)引入一定的技術(shù)復(fù)雜度,一般來(lái)說(shuō)從兩個(gè)方面來(lái)判斷是否需要使用緩存:
CPU 占用
如果你有某些應(yīng)用需要消耗大量的 CPU 去計(jì)算,比如正則表達(dá)式;如果你使用正則表達(dá)式比較頻繁,而它又占用了很多 CPU 的話,那你就應(yīng)該使用緩存將正則表達(dá)式的結(jié)果給緩存下來(lái)。
數(shù)據(jù)庫(kù) IO 占用
如果你發(fā)現(xiàn)你的數(shù)據(jù)庫(kù)連接池比較空閑,可以不用緩存。但是如果數(shù)據(jù)庫(kù)連接池比較繁忙,甚至經(jīng)常報(bào)出連接不夠的報(bào)警,那么是時(shí)候應(yīng)該考慮緩存了。
筆者曾經(jīng)有個(gè)服務(wù)被很多其他服務(wù)調(diào)用,其他時(shí)間都還好,但是在每天早上 10 點(diǎn)的時(shí)候總是會(huì)報(bào)出數(shù)據(jù)庫(kù)連接池連接不夠的報(bào)警。
經(jīng)過(guò)排查,我發(fā)現(xiàn)有幾個(gè)服務(wù)選擇了在 10 點(diǎn)做定時(shí)任務(wù),大量的請(qǐng)求打過(guò)來(lái),DB 連接池不夠,從而產(chǎn)生連接池不夠的報(bào)警。
這個(gè)時(shí)候有幾個(gè)選擇,我們可以通過(guò)擴(kuò)容機(jī)器來(lái)解決,也可以通過(guò)增加數(shù)據(jù)庫(kù)連接池來(lái)解決。
但是沒(méi)有必要增加這些成本,因?yàn)橹挥性?10 點(diǎn)的時(shí)候才會(huì)出現(xiàn)這個(gè)問(wèn)題。后來(lái)引入了緩存,不僅解決了這個(gè)問(wèn)題,而且還增加了讀的性能。
如果并沒(méi)有上述兩個(gè)問(wèn)題,那么你不必為了增加緩存而緩存。
如何選擇合適的緩存
緩存分為進(jìn)程內(nèi)緩存和分布式緩存。包括筆者在內(nèi)的很多人在開(kāi)始選緩存框架的時(shí)候都會(huì)感到困惑:網(wǎng)上的緩存太多了,大家都吹噓自己很牛逼,我該怎么選擇呢?
選擇合適的進(jìn)程緩存
首先看幾個(gè)比較常用緩存的比較,具體原理可以參考《你應(yīng)該知道的緩存進(jìn)化史》:
對(duì)于 ConcurrentHashMap 來(lái)說(shuō),比較適合緩存比較固定不變的元素,且緩存的數(shù)量較小的。
雖然從上面表格中比起來(lái)有點(diǎn)遜色,但是由于它是 JDK 自帶的類,在各種框架中依然有大量的使用。
比如我們可以用來(lái)緩存反射的 Method,F(xiàn)ield 等等;也可以緩存一些鏈接,防止重復(fù)建立。在 Caffeine 中也是使用的 ConcurrentHashMap 來(lái)存儲(chǔ)元素。
對(duì)于 LRUMap 來(lái)說(shuō),如果不想引入第三方包,又想使用淘汰算法淘汰數(shù)據(jù),可以使用這個(gè)。
對(duì)于 Ehcache 來(lái)說(shuō),由于其 jar 包很大,較重量級(jí)。對(duì)于需要持久化和集群的一些功能的,可以選擇 Ehcache。
筆者沒(méi)怎么使用過(guò)這個(gè)緩存,如果要選擇的話,可以選擇分布式緩存來(lái)替代 Ehcache。
對(duì)于 Guava Cache 來(lái)說(shuō),Guava 這個(gè) jar 包在很多 Java 應(yīng)用程序中都有大量的引入。
所以很多時(shí)候直接用就好了,并且它本身是輕量級(jí)的而且功能較為豐富,在不了解 Caffeine 的情況下可以選擇 Guava Cache。
對(duì)于 Caffeine 來(lái)說(shuō),筆者是非常推薦的,它在***率,讀寫(xiě)性能上都比 Guava Cache 好很多。
并且它的 API 和 Guava Cache 基本一致,甚至?xí)嘁稽c(diǎn)。在真實(shí)環(huán)境中使用 Caffeine,取得過(guò)不錯(cuò)的效果。
總結(jié)一下:如果不需要淘汰算法則選擇 ConcurrentHashMap;如果需要淘汰算法和一些豐富的 API,這里推薦選擇 Caffeine。
選擇合適的分布式緩存
這里我選取三個(gè)比較出名的分布式緩存來(lái)作為比較,MemCache(沒(méi)有實(shí)戰(zhàn)使用過(guò)),Redis(在美團(tuán)又叫 Squirrel),Tair(在美團(tuán)又叫 Cellar)。
不同的分布式緩存功能特性和實(shí)現(xiàn)原理方面有很大的差異,因此它們所適應(yīng)的場(chǎng)景也有所不同:
- MemCache:這一塊接觸得比較少,不做過(guò)多的推薦。其吞吐量較大,但是支持的數(shù)據(jù)結(jié)構(gòu)較少,并且不支持持久化。
- Redis:支持豐富的數(shù)據(jù)結(jié)構(gòu),讀寫(xiě)性能很高,但是數(shù)據(jù)全內(nèi)存,必須要考慮資源成本,支持持久化。
- Tair:支持豐富的數(shù)據(jù)結(jié)構(gòu),讀寫(xiě)性能較高,部分類型比較慢,理論上容量可以***擴(kuò)充。
總結(jié):如果服務(wù)對(duì)延遲比較敏感,Map/Set 數(shù)據(jù)也比較多的話,比較適合 Redis。
如果服務(wù)需要放入緩存量的數(shù)據(jù)很大,對(duì)延遲又不是特別敏感的話,那就可以選擇 Tair。
在美團(tuán)的很多應(yīng)用中對(duì) Tair 都有應(yīng)用,在筆者的項(xiàng)目中使用其存放我們生成的支付 Token,支付碼,用來(lái)替代數(shù)據(jù)庫(kù)存儲(chǔ)。大部分的情況下兩者都可以選擇,互為替代。
多級(jí)緩存
一說(shuō)到緩存,很多人腦子里面馬上就會(huì)出現(xiàn)下面的圖:
Redis 用來(lái)存儲(chǔ)熱點(diǎn)數(shù)據(jù),Redis 中沒(méi)有的數(shù)據(jù)則直接去數(shù)據(jù)庫(kù)訪問(wèn)。
在之前介紹本地緩存的時(shí)候,很多人都問(wèn)我,我已經(jīng)有 Redis 了,我為什么還需要了解 Guava,Caffeine 這些進(jìn)程緩存呢?
我統(tǒng)一回復(fù)下,有如下兩個(gè)原因:
- Redis 如果掛了或者使用老版本的 Redis,會(huì)進(jìn)行全量同步,此時(shí) Redis 是不可用的,這個(gè)時(shí)候我們只能訪問(wèn)數(shù)據(jù)庫(kù),很容易造成雪崩。
- 訪問(wèn) Redis 會(huì)有一定的網(wǎng)絡(luò) I/O 以及序列化反序列化,雖然性能很高但是終究沒(méi)有本地方法快,可以將最熱的數(shù)據(jù)存放在本地,以便進(jìn)一步加快訪問(wèn)速度。
這個(gè)思路并不是我們做互聯(lián)網(wǎng)架構(gòu)獨(dú)有的,在計(jì)算機(jī)系統(tǒng)中使用 L1,L2,L3 多級(jí)緩存,用來(lái)減少對(duì)內(nèi)存的直接訪問(wèn),從而加快訪問(wèn)速度。
所以如果僅僅是使用 Redis,能滿足我們大部分需求,但是當(dāng)需要追求更高性能以及更高可用性的時(shí)候,那就不得不了解多級(jí)緩存。
使用進(jìn)程緩存
對(duì)于進(jìn)程內(nèi)緩存,它本來(lái)受限于內(nèi)存大小的限制,以及進(jìn)程緩存更新后其他緩存無(wú)法得知,所以一般來(lái)說(shuō)進(jìn)程緩存適用于:
數(shù)據(jù)量不是很大,數(shù)據(jù)更新頻率較低,之前我們有個(gè)查詢商家名字的服務(wù),在發(fā)送短信的時(shí)候需要調(diào)用,由于商家名字變更頻率較低,并且就算是變更了沒(méi)有及時(shí)變更緩存,短信里面帶有老的商家名字客戶也能接受。
利用 Caffeine 作為本地緩存,Size 設(shè)置為 1 萬(wàn),過(guò)期時(shí)間設(shè)置為 1 個(gè)小時(shí),基本能在高峰期解決問(wèn)題。
如果數(shù)據(jù)量更新頻繁,也想使用進(jìn)程緩存的話,那么可以將其過(guò)期時(shí)間設(shè)置為較短,或者設(shè)置其較短的自動(dòng)刷新的時(shí)間。這些對(duì)于 Caffeine 或者 Guava Cache 來(lái)說(shuō)都是現(xiàn)成的 API。
使用多級(jí)緩存
俗話說(shuō)得好,世界上沒(méi)有什么是一個(gè)緩存解決不了的事,如果有,那就兩個(gè)。
一般來(lái)說(shuō)我們選擇一個(gè)進(jìn)程緩存和一個(gè)分布式緩存來(lái)搭配做多級(jí)緩存,一般來(lái)說(shuō)引入兩個(gè)也足夠了。
如果使用三個(gè),四個(gè)的話,技術(shù)維護(hù)成本會(huì)很高,反而有可能會(huì)得不償失,如下圖所示:
利用 Caffeine 做一級(jí)緩存,Redis 作為二級(jí)緩存,步驟如下:
- 首先去 Caffeine 中查詢數(shù)據(jù),如果有直接返回。如果沒(méi)有則進(jìn)行第 2 步。
- 再去 Redis 中查詢,如果查詢到了返回?cái)?shù)據(jù)并在 Caffeine 中填充此數(shù)據(jù)。如果沒(méi)有查到則進(jìn)行第 3 步。
- ***去 MySQL 中查詢,如果查詢到了返回?cái)?shù)據(jù)并在 Redis,Caffeine 中依次填充此數(shù)據(jù)。
對(duì)于 Caffeine 的緩存,如果有數(shù)據(jù)更新,只能刪除更新數(shù)據(jù)的那臺(tái)機(jī)器上的緩存,其他機(jī)器只能通過(guò)超時(shí)來(lái)過(guò)期緩存,超時(shí)設(shè)定可以有兩種策略:
- 設(shè)置成寫(xiě)入后多少時(shí)間后過(guò)期。
- 設(shè)置成寫(xiě)入后多少時(shí)間刷新。
對(duì)于 Redis 的緩存更新,其他機(jī)器立刻可見(jiàn),但是也必須要設(shè)置超時(shí)時(shí)間,其時(shí)間比 Caffeine 的過(guò)期長(zhǎng)。
為了解決進(jìn)程內(nèi)緩存的問(wèn)題,設(shè)計(jì)進(jìn)一步優(yōu)化:
通過(guò) Redis 的 Pub/Sub,可以通知其他進(jìn)程緩存對(duì)此緩存進(jìn)行刪除。如果 Redis 掛了或者訂閱機(jī)制不靠譜,依靠超時(shí)設(shè)定,依然可以做兜底處理。
緩存更新
一般來(lái)說(shuō)緩存的更新有兩種情況:
- 先刪除緩存,再更新數(shù)據(jù)庫(kù)。
- 先更新數(shù)據(jù)庫(kù),再刪除緩存。
這兩種情況在業(yè)界,大家都有自己的看法。具體怎么使用還得看各自的取舍。當(dāng)然肯定有人會(huì)問(wèn)為什么要?jiǎng)h除緩存呢?而不是更新緩存呢?
當(dāng)有多個(gè)并發(fā)的請(qǐng)求更新數(shù)據(jù),你并不能保證更新數(shù)據(jù)庫(kù)的順序和更新緩存的順序一致,那么就會(huì)出現(xiàn)數(shù)據(jù)庫(kù)中和緩存中數(shù)據(jù)不一致的情況。所以一般來(lái)說(shuō)考慮刪除緩存。
先刪除緩存,再更新數(shù)據(jù)庫(kù)
對(duì)于一個(gè)更新操作簡(jiǎn)單來(lái)說(shuō),就是先對(duì)各級(jí)緩存進(jìn)行刪除,然后更新數(shù)據(jù)庫(kù)。
這個(gè)操作有一個(gè)比較大的問(wèn)題,在對(duì)緩存刪除完之后,有一個(gè)讀請(qǐng)求,這個(gè)時(shí)候由于緩存被刪除所以直接會(huì)讀庫(kù),讀操作的數(shù)據(jù)是老的并且會(huì)被加載進(jìn)入緩存當(dāng)中,后續(xù)讀請(qǐng)求全部訪問(wèn)的老數(shù)據(jù)。
對(duì)緩存的操作不論成功失敗都不能阻塞我們對(duì)數(shù)據(jù)庫(kù)的操作,那么很多時(shí)候刪除緩存可以用異步的操作,但是先刪除緩存不能很好的適用于這個(gè)場(chǎng)景。
先刪除緩存也有一個(gè)好處是,如果對(duì)數(shù)據(jù)庫(kù)操作失敗了,那么由于先刪除的緩存,最多只是造成 Cache Miss。
先更新數(shù)據(jù)庫(kù),再刪除緩存(推薦)
如果我們使用更新數(shù)據(jù)庫(kù),再刪除緩存就能避免上面的問(wèn)題。但是同樣引入了新的問(wèn)題。
試想一下有一個(gè)數(shù)據(jù)此時(shí)是沒(méi)有緩存的,所以查詢請(qǐng)求會(huì)直接落庫(kù),更新操作在查詢請(qǐng)求之后,但是更新操作刪除數(shù)據(jù)庫(kù)操作在查詢完之后回填緩存之前,就會(huì)導(dǎo)致我們緩存中和數(shù)據(jù)庫(kù)出現(xiàn)緩存不一致。
為什么我們這種情況有問(wèn)題,很多公司包括 Facebook 還會(huì)選擇呢?因?yàn)橐|發(fā)這個(gè)條件比較苛刻:
- 首先需要數(shù)據(jù)不在緩存中。
- 其次查詢操作需要在更新操作先到達(dá)數(shù)據(jù)庫(kù)。
- ***查詢操作的回填比更新操作的刪除后觸發(fā),這個(gè)條件基本很難出現(xiàn),因?yàn)楦虏僮鞯谋緛?lái)在查詢操作之后,一般來(lái)說(shuō)更新操作比查詢操作稍慢。但是更新操作的刪除卻在查詢操作之后,所以這個(gè)情況比較少出現(xiàn)。
對(duì)比上面先刪除緩存,再更新數(shù)據(jù)庫(kù)的問(wèn)題來(lái)說(shuō)這種問(wèn)題出現(xiàn)的概率很低,況且我們有超時(shí)機(jī)制保底所以基本能滿足我們的需求。
如果真的需要追求***,可以使用二階段提交,但是成本和收益一般來(lái)說(shuō)不成正比。
當(dāng)然還有個(gè)問(wèn)題是如果我們刪除失敗了,緩存的數(shù)據(jù)就會(huì)和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致,那么我們就只能靠過(guò)期超時(shí)來(lái)進(jìn)行兜底。
對(duì)此我們可以進(jìn)行優(yōu)化,如果刪除失敗的話 我們不能影響主流程那么我們可以將其放入隊(duì)列后續(xù)進(jìn)行異步刪除。
緩存挖坑三劍客
大家一聽(tīng)到緩存有哪些注意事項(xiàng),首先想到的肯定是緩存穿透,緩存擊穿,緩存雪崩這三個(gè)挖坑的小能手,這里簡(jiǎn)單介紹一下他們具體是什么以及應(yīng)對(duì)的方法。
緩存穿透
緩存穿透是指查詢的數(shù)據(jù)在數(shù)據(jù)庫(kù)是沒(méi)有的,那么在緩存中自然也沒(méi)有,所以在緩存中查不到就會(huì)去數(shù)據(jù)庫(kù)查詢,這樣的請(qǐng)求一多,我們數(shù)據(jù)庫(kù)的壓力自然會(huì)增大。
為了避免這個(gè)問(wèn)題,可以采取下面兩個(gè)手段:
約定:對(duì)于返回為 NULL 的依然緩存,對(duì)于拋出異常的返回不進(jìn)行緩存,注意不要把拋異常的也給緩存了。
采用這種手段會(huì)增加我們緩存的維護(hù)成本,需要在插入緩存的時(shí)候刪除這個(gè)空緩存,當(dāng)然我們可以通過(guò)設(shè)置較短的超時(shí)時(shí)間來(lái)解決這個(gè)問(wèn)題。
制定一些規(guī)則過(guò)濾一些不可能存在的數(shù)據(jù),小數(shù)據(jù)用 BitMap,大數(shù)據(jù)可以用布隆過(guò)濾器。
比如你的訂單 ID 明顯是在一個(gè)范圍 1-1000,如果不是 1-1000 之內(nèi)的數(shù)據(jù)那其實(shí)可以直接給過(guò)濾掉。
緩存擊穿
對(duì)于某些 Key 設(shè)置了過(guò)期時(shí)間,但是它是熱點(diǎn)數(shù)據(jù),如果某個(gè) Key 失效,可能大量的請(qǐng)求打過(guò)來(lái),緩存未***,然后去數(shù)據(jù)庫(kù)訪問(wèn),此時(shí)數(shù)據(jù)庫(kù)訪問(wèn)量會(huì)急劇增加。
為了避免這個(gè)問(wèn)題,我們可以采取下面的兩個(gè)手段:
- 加分布式鎖:加載數(shù)據(jù)的時(shí)候可以利用分布式鎖鎖住這個(gè)數(shù)據(jù)的 Key,在 Redis 中直接使用 SetNX 操作即可。
對(duì)于獲取到這個(gè)鎖的線程,查詢數(shù)據(jù)庫(kù)更新緩存,其他線程采取重試策略,這樣數(shù)據(jù)庫(kù)不會(huì)同時(shí)受到很多線程訪問(wèn)同一條數(shù)據(jù)。
- 異步加載:由于緩存擊穿是熱點(diǎn)數(shù)據(jù)才會(huì)出現(xiàn)的問(wèn)題,可以對(duì)這部分熱點(diǎn)數(shù)據(jù)采取到期自動(dòng)刷新的策略,而不是到期自動(dòng)淘汰。淘汰也是為了數(shù)據(jù)的時(shí)效性,所以采用自動(dòng)刷新也可以。
緩存雪崩
緩存雪崩是指緩存不可用或者大量緩存由于超時(shí)時(shí)間相同在同一時(shí)間段失效,大量請(qǐng)求直接訪問(wèn)數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)壓力過(guò)大導(dǎo)致系統(tǒng)雪崩。
為了避免這個(gè)問(wèn)題,我們采取下面的手段:
- 增加緩存系統(tǒng)可用性,通過(guò)監(jiān)控關(guān)注緩存的健康程度,根據(jù)業(yè)務(wù)量適當(dāng)?shù)臄U(kuò)容緩存。
- 采用多級(jí)緩存,不同級(jí)別緩存設(shè)置的超時(shí)時(shí)間不同,即使某個(gè)級(jí)別緩存都過(guò)期,也有其他級(jí)別緩存兜底。
- 緩存的 Key 值可以取個(gè)隨機(jī)值,比如以前是設(shè)置 10 分鐘的超時(shí)時(shí)間,那每個(gè) Key 都可以隨機(jī) 8-13 分鐘過(guò)期,盡量讓不同 Key 的過(guò)期時(shí)間不同。
緩存污染
緩存污染一般出現(xiàn)在我們使用本地緩存中。可以想象,在本地緩存中如果你獲得了緩存,但是你接下來(lái)修改了這個(gè)數(shù)據(jù),這個(gè)數(shù)據(jù)卻并沒(méi)有更新在數(shù)據(jù)庫(kù),這樣就造成了緩存污染:
上面的代碼就造成了緩存污染,通過(guò) ID 獲取 Customer,但是需求需要修改 Customer 的名字。
所以開(kāi)發(fā)人員直接在取出來(lái)的對(duì)象中直接修改,這個(gè) Customer 對(duì)象就會(huì)被污染,其他線程取出這個(gè)數(shù)據(jù)就是錯(cuò)誤的數(shù)據(jù)。
要想避免這個(gè)問(wèn)題需要開(kāi)發(fā)人員從編碼上注意,并且代碼必須經(jīng)過(guò)嚴(yán)格的 Review,以及全方位的回歸測(cè)試,才能從一定程度上解決這個(gè)問(wèn)題。
序列化
序列化是很多人都不注意的一個(gè)問(wèn)題,很多人忽略了序列化的問(wèn)題,上線之后馬上報(bào)出一下奇怪的錯(cuò)誤異常,造成了不必要的損失,***一排查都是序列化的問(wèn)題。
列舉幾個(gè)序列化常見(jiàn)的問(wèn)題:
Key-Value 對(duì)象過(guò)于復(fù)雜導(dǎo)致序列化不支持:筆者之前出過(guò)一個(gè)問(wèn)題,在美團(tuán)的 Tair 內(nèi)部默認(rèn)是使用 protostuff 進(jìn)行序列化。
而美團(tuán)使用的通訊框架是 thfift,thrift 的 TO 是自動(dòng)生成的,這個(gè) TO 里面有很多復(fù)雜的數(shù)據(jù)結(jié)構(gòu),但是將它存放到了 Tair 中。
查詢的時(shí)候反序列化也沒(méi)有報(bào)錯(cuò),單測(cè)也通過(guò),但是到 QA 測(cè)試的時(shí)候發(fā)現(xiàn)這一塊功能有問(wèn)題,有個(gè)字段是 boolean 類型默認(rèn)是 False,把它改成 true 之后,序列化到 Tair 中再反序列化還是 False。
定位到是 protostuff 對(duì)于復(fù)雜結(jié)構(gòu)的對(duì)象(比如數(shù)組,List 等等)支持不是很好,會(huì)造成一定的問(wèn)題。
后來(lái)對(duì)這個(gè) TO 進(jìn)行了轉(zhuǎn)換,用普通的 Java 對(duì)象就能進(jìn)行正確的序列化反序列化。
添加了字段或者刪除了字段,導(dǎo)致上線之后老的緩存獲取的時(shí)候反序列化報(bào)錯(cuò),或者出現(xiàn)一些數(shù)據(jù)移位。
不同的 JVM 的序列化不同,如果你的緩存有不同的服務(wù)都在共同使用(不提倡),那么需要注意不同 JVM 可能會(huì)對(duì) Class 內(nèi)部的 Field 排序不同,而影響序列化。
比如(舉例,實(shí)際情況不一定如此)下面的代碼,在 JDK7 和 JDK8 中對(duì)象 A 的排列順序不同,最終會(huì)導(dǎo)致反序列化結(jié)果出現(xiàn)問(wèn)題:
- //jdk 7
- class A{
- int a;
- int b;
- }
- //jdk 8
- class A{
- int b;
- int a;
- }
序列化的問(wèn)題必須得到重視,解決的辦法有如下幾點(diǎn):
測(cè)試:對(duì)于序列化需要進(jìn)行全面的測(cè)試,如果有不同的服務(wù)并且他們的 JVM 不同,那么你也需要做這一塊的測(cè)試。
在上面的問(wèn)題中筆者的單測(cè)通過(guò)的原因是用的默認(rèn)數(shù)據(jù) False,所以根本沒(méi)有測(cè)試 true 的情況,還好 QA 給力,將它給測(cè)試出來(lái)了。
對(duì)于不同的序列化框架都有自己不同的原理,對(duì)于添加字段之后如果當(dāng)前序列化框架不能兼容老的,那么可以換個(gè)序列化框架。
對(duì)于 protostuff 來(lái)說(shuō)它是按照 Field 的順序來(lái)進(jìn)行反序列化的,對(duì)于添加字段我們需要放到末尾,也就是不能插在中間,否則會(huì)出現(xiàn)錯(cuò)誤。
對(duì)于刪除字段來(lái)說(shuō),用 @Deprecated 注解進(jìn)行標(biāo)注棄用,如果貿(mào)然刪除,除非是***一個(gè)字段,否則肯定會(huì)出現(xiàn)序列化異常。
可以使用雙寫(xiě)來(lái)避免,對(duì)于每個(gè)緩存的 Key 值可以加上版本號(hào),每次上線版本號(hào)都加 1。
比如現(xiàn)在線上的緩存用的是 Key_1,即將要上線的是 Key_2,上線之后對(duì)緩存的添加是會(huì)寫(xiě)新老兩個(gè)不同的版本(Key_1,Key_2)的 Key-Value,讀取數(shù)據(jù)還是讀取老版本 Key_1 的數(shù)據(jù)。
假設(shè)之前的緩存的過(guò)期時(shí)間是半個(gè)小時(shí),那么上線半個(gè)小時(shí)之后,之前的老緩存存量的數(shù)據(jù)都會(huì)被淘汰,此時(shí)線上老緩存和新緩存的數(shù)據(jù)基本是一樣的,切換讀操作到新緩存,然后停止雙寫(xiě)。
采用這種方法基本能平滑過(guò)渡新老 Model 交替,但是不好的就是需要短暫的維護(hù)兩套新老 Model,下次上線的時(shí)候需要?jiǎng)h除掉老 Model,這樣增加了維護(hù)成本。
GC 調(diào)優(yōu)
對(duì)于大量使用本地緩存的應(yīng)用,由于涉及到緩存淘汰,那么 GC 問(wèn)題必定是常事。如果出現(xiàn) GC 較多,STW 時(shí)間較長(zhǎng),那么必定會(huì)影響服務(wù)可用性。
這一塊給出下面幾點(diǎn)建議:
- 經(jīng)常查看 GC 監(jiān)控,如何發(fā)現(xiàn)不正常,需要想辦法對(duì)其進(jìn)行優(yōu)化。
- 對(duì)于 CMS 垃圾收集算法,如果發(fā)現(xiàn) Remark 過(guò)長(zhǎng),如果是大量本地緩存應(yīng)用的話這個(gè)過(guò)長(zhǎng)應(yīng)該很正常,因?yàn)樵诓l(fā)階段很容易有很多新對(duì)象進(jìn)入緩存,從而 Remark 階段掃描很耗時(shí),Remark 又會(huì)暫停。
可以開(kāi)啟 XX:CMSScavengeBeforeRemark,在 Remark 階段前進(jìn)行一次 YGC,從而減少 Remark 階段掃描 GC Root 的開(kāi)銷。
- 可以使用 G1 垃圾收集算法,通過(guò) XX:MaxGCPauseMillis 設(shè)置***停頓時(shí)間,提高服務(wù)可用性。
緩存的監(jiān)控
很多人對(duì)于緩存的監(jiān)控也比較忽略,基本上線之后如果不報(bào)錯(cuò),然后就默認(rèn)它就生效了。
但是存在這個(gè)問(wèn)題,很多人由于經(jīng)驗(yàn)不足,有可能設(shè)置了不恰當(dāng)?shù)倪^(guò)期時(shí)間,或者不恰當(dāng)?shù)木彺娲笮?dǎo)致緩存***率不高,讓緩存成為了代碼中的一個(gè)裝飾品。
所以對(duì)于緩存各種指標(biāo)的監(jiān)控,也比較重要,通過(guò)不同的指標(biāo)數(shù)據(jù),我們可以對(duì)緩存的參數(shù)進(jìn)行優(yōu)化,從而讓緩存達(dá)到***化:
上面的代碼中用來(lái)記錄 Get 操作的,通過(guò) Cat 記錄了獲取緩存成功,緩存不存在,緩存過(guò)期,緩存失敗(獲取緩存時(shí)如果拋出異常,則叫失敗)。
通過(guò)這些指標(biāo),我們就能統(tǒng)計(jì)出***率,我們調(diào)整過(guò)期時(shí)間和大小的時(shí)候就可以參考這些指標(biāo)進(jìn)行優(yōu)化。
一款好的框架
一個(gè)好的劍客沒(méi)有一把好劍怎么行呢?如果要使用好緩存,一個(gè)好的框架也必不可少。
在最開(kāi)始使用的時(shí)候,大家使用緩存都用一些 util,把緩存的邏輯寫(xiě)在業(yè)務(wù)邏輯中:
上面的代碼把緩存的邏輯耦合在業(yè)務(wù)邏輯當(dāng)中,如果我們要增加成多級(jí)緩存那就需要修改我們的業(yè)務(wù)邏輯,不符合開(kāi)閉原則,所以引入一個(gè)好的框架是不錯(cuò)的選擇。
推薦大家使用 JetCache 這款開(kāi)源框架,它實(shí)現(xiàn)了 Java 緩存規(guī)范 JSR107 并且支持自動(dòng)刷新等高級(jí)功能。
筆者參考 JetCache 結(jié)合 Spring Cache,監(jiān)控框架 Cat 以及美團(tuán)的熔斷限流框架 Rhino 實(shí)現(xiàn)了一套自有的緩存框架,讓操作緩存,打點(diǎn)監(jiān)控,熔斷降級(jí),業(yè)務(wù)人員無(wú)需關(guān)心。
上面的代碼可以優(yōu)化成:
對(duì)于一些監(jiān)控?cái)?shù)據(jù)也能輕松從大盤(pán)上看到:
總結(jié)
想要真正的使用好一個(gè)緩存,必須要掌握很多的知識(shí),并不是看幾個(gè) Redis 原理分析,就能把 Redis 緩存用得爐火純青。
對(duì)于不同場(chǎng)景,緩存有各自不同的用法,同樣的不同的緩存也有自己的調(diào)優(yōu)策略,進(jìn)程內(nèi)緩存你需要關(guān)注的是它的淘汰算法和 GC 調(diào)優(yōu),以及要避免緩存污染等。
分布式緩存你需要關(guān)注的是它的高可用,如果它不可用了,如何進(jìn)行降級(jí),以及一些序列化的問(wèn)題。
一個(gè)好的框架也是必不可少的,對(duì)它如果使用得當(dāng)再加上上面介紹的經(jīng)驗(yàn),相信能讓你很好的駕馭住這頭野馬——緩存。
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】