一個(gè)架構(gòu)師的緩存修煉之路
一位七牛的資深架構(gòu)師曾經(jīng)說(shuō)過(guò)這樣一句話:
“Nginx+業(yè)務(wù)邏輯層+數(shù)據(jù)庫(kù)+緩存層+消息隊(duì)列,這種模型幾乎能適配絕大部分的業(yè)務(wù)場(chǎng)景。
這么多年過(guò)去了,這句話或深或淺地影響了我的技術(shù)選擇,以至于后來(lái)我花了很多時(shí)間去重點(diǎn)學(xué)習(xí)緩存相關(guān)的技術(shù)。
我在10年前開(kāi)始使用緩存,從本地緩存、到分布式緩存、再到多級(jí)緩存,踩過(guò)很多坑。下面我結(jié)合自己使用緩存的歷程,談?wù)勎覍?duì)緩存的認(rèn)識(shí)。
01?本地緩存
1. 頁(yè)面級(jí)緩存
"foobar"
??????some jsp?content?
2. 對(duì)象緩存
3. 刷新策略
2018年,我和我的小伙伴自研了配置中心,為了讓客戶端以最快的速度讀取配置, 本地緩存使用了 Guava,整體架構(gòu)如下圖所示:
那本地緩存是如何更新的呢?有兩種機(jī)制:
-
客戶端啟動(dòng)定時(shí)任務(wù),從配置中心拉取數(shù)據(jù)。 -
當(dāng)配置中心有數(shù)據(jù)變化時(shí),主動(dòng)推送給客戶端。這里我并沒(méi)有使用websocket,而是使用了 RocketMQ Remoting 通訊框架。

▍zookeeper watch機(jī)制
▍websocket 機(jī)制
websocket 和 zookeeper 機(jī)制有點(diǎn)類似,當(dāng)網(wǎng)關(guān)與 admin 首次建立好 websocket 連接時(shí),admin 會(huì)推送一次全量數(shù)據(jù),后續(xù)如果配置數(shù)據(jù)發(fā)生變更,則將增量數(shù)據(jù)通過(guò) websocket 主動(dòng)推送給 soul-web。
http請(qǐng)求到達(dá)服務(wù)端后,并不是馬上響應(yīng),而是利用 Servlet 3.0 的異步機(jī)制響應(yīng)數(shù)據(jù)。當(dāng)配置發(fā)生變化時(shí),服務(wù)端會(huì)挨個(gè)移除隊(duì)列中的長(zhǎng)輪詢請(qǐng)求,告知是哪個(gè) Group 的數(shù)據(jù)發(fā)生了變更,網(wǎng)關(guān)收到響應(yīng)后,再次請(qǐng)求該 Group 的配置數(shù)據(jù)。
-
pull 模式必不可少 -
增量推送大同小異
02 分布式緩存
1.? 合理控制對(duì)象大小及讀取策略
1、數(shù)據(jù)格式非常精簡(jiǎn),只返回給前端必要的數(shù)據(jù),部分?jǐn)?shù)據(jù)通過(guò)數(shù)組的方式返回
2、使用 websocket,進(jìn)入頁(yè)面后推送全量數(shù)據(jù),數(shù)據(jù)發(fā)生變化推送增量數(shù)據(jù)
再回到我的問(wèn)題上,最終是用什么方案解決的呢?當(dāng)時(shí),我們的比分直播模塊緩存格式是 JSON 數(shù)組,每個(gè)數(shù)組元素包含 20 多個(gè)鍵值對(duì), 下面的 JSON 示例我僅僅列了其中 4 個(gè)屬性。
[{
"playId":"2399",
"guestTeamName":"小牛",
"hostTeamName":"湖人",
"europe":"123"
}]
這種數(shù)據(jù)結(jié)構(gòu),一般情況下沒(méi)有什么問(wèn)題。但是當(dāng)字段數(shù)多達(dá) 20 多個(gè),而且每天的比賽場(chǎng)次非常多時(shí),在高并發(fā)的請(qǐng)求下其實(shí)很容易引發(fā)問(wèn)題。
基于工期以及風(fēng)險(xiǎn)考慮,最終我們采用了比較保守的優(yōu)化方案:
[["2399","小牛","湖人","123"]]
修改完成之后, 緩存的大小從平均 300k 左右降為 80k 左右,YGC 頻率下降很明顯,同時(shí)頁(yè)面響應(yīng)也變快了很多。
但過(guò)了一會(huì),cpu load 會(huì)在瞬間波動(dòng)得比較高。可見(jiàn),雖然我們減少了緩存大小,但是讀取大對(duì)象依然對(duì)系統(tǒng)資源是極大的損耗,導(dǎo)致 Full GC 的頻率也不低。?
3)為了徹底解決這個(gè)問(wèn)題,我們使用了更精細(xì)化的緩存讀取策略。
我們把緩存拆成兩個(gè)部分,第一部分是全量數(shù)據(jù),第二部分是增量數(shù)據(jù)(數(shù)據(jù)量很?。m?yè)面第一次請(qǐng)求拉取全量數(shù)據(jù),當(dāng)比分有變化的時(shí)候,通過(guò) websocket 推送增量數(shù)據(jù)。
第 3 步完成后,頁(yè)面的訪問(wèn)速度極快,服務(wù)器的資源使用也很少,優(yōu)化的效果非常優(yōu)異。
2.? 分頁(yè)列表查詢
select?id?from?blogs?limit?0,10?
select?id?from?blogs?where?id?in?(noHitId1,?noHitId2)
-
本地緩存:性能極高,for 循環(huán)即可 -
memcached:使用 mget 命令 -
Redis:若緩存對(duì)象結(jié)構(gòu)簡(jiǎn)單,使用 mget 、hmget命令;若結(jié)構(gòu)復(fù)雜,可以考慮使用 pipleline,lua腳本模式
03 多級(jí)緩存
緩存讀取流程如下:
1、業(yè)務(wù)網(wǎng)關(guān)剛啟動(dòng)時(shí),本地緩存沒(méi)有數(shù)據(jù),讀取 Redis 緩存,如果 Redis 緩存也沒(méi)數(shù)據(jù),則通過(guò) RPC 調(diào)用導(dǎo)購(gòu)服務(wù)讀取數(shù)據(jù),然后再將數(shù)據(jù)寫(xiě)入本地緩存和 Redis 中;若 Redis 緩存不為空,則將緩存數(shù)據(jù)寫(xiě)入本地緩存中。
2、由于步驟1已經(jīng)對(duì)本地緩存預(yù)熱,后續(xù)請(qǐng)求直接讀取本地緩存,返回給用戶端。
3、Guava 配置了 refresh 機(jī)制,每隔一段時(shí)間會(huì)調(diào)用自定義 LoadingCache 線程池(5個(gè)最大線程,5個(gè)核心線程)去導(dǎo)購(gòu)服務(wù)同步數(shù)據(jù)到本地緩存和 Redis 中。
優(yōu)化后,性能表現(xiàn)很好,平均耗時(shí)在 5ms 左右。最開(kāi)始我以為出現(xiàn)問(wèn)題的幾率很小,可是有一天晚上,突然發(fā)現(xiàn) app 端首頁(yè)顯示的數(shù)據(jù)時(shí)而相同,時(shí)而不同。
1、惰性加載仍然可能造成多臺(tái)機(jī)器的數(shù)據(jù)不一致
2、 LoadingCache 線程池?cái)?shù)量配置的不太合理,? 導(dǎo)致了線程堆積
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒(méi)關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:
長(zhǎng)按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!