[導(dǎo)讀]一位七牛的個架構(gòu)師資深架構(gòu)師曾經(jīng)說過這樣一句話:Nginx+業(yè)務(wù)邏輯層+數(shù)據(jù)庫+緩存層+消息隊(duì)列,這種模型幾乎能適配絕大部分的存修業(yè)務(wù)場景。這么多年過去了,煉之路這句話或深或淺地影響了我的個架構(gòu)師技術(shù)選擇,以至于后來我花了很多時間去重點(diǎn)學(xué)習(xí)緩存相關(guān)的存修技術(shù)。我在10年前開始使用緩存,煉之路從本地緩存、個架構(gòu)師到分布式緩存、存修再到多級緩存,煉之路踩過很多坑。個架構(gòu)師下面我結(jié)合自己使用緩存的存修歷程,談?wù)勎覍彺娴臒捴氛J(rèn)識。
一位七牛的個架構(gòu)師資深架構(gòu)師曾經(jīng)說過這樣一句話:

“ Nginx+業(yè)務(wù)邏輯層+數(shù)據(jù)庫+緩存層+消息隊(duì)列,這種模型幾乎能適配絕大部分的存修業(yè)務(wù)場景。
這么多年過去了,煉之路這句話或深或淺地影響了我的技術(shù)選擇,以至于后來我花了很多時間去重點(diǎn)學(xué)習(xí)緩存相關(guān)的技術(shù)。
我在10年前開始使用緩存,從本地緩存、到分布式緩存、再到多級緩存,踩過很多坑。下面我結(jié)合自己使用緩存的歷程,談?wù)勎覍彺娴恼J(rèn)識。
01?本地緩存
1. 頁面級緩存
我使用緩存的時間很早,2010年左右使用過 OSCache,當(dāng)時主要用在 JSP 頁面中用于實(shí)現(xiàn)頁面級緩存。偽代碼類似這樣: 中間的那段 JSP 代碼將會以 key="foobar" 緩存在 session 中,這樣其他頁面就能共享這段緩存內(nèi)容。 在使用 JSP 這種遠(yuǎn)古技術(shù)的場景下,通過引入 OSCache 之后 ,頁面的加載速度確實(shí)提升很快。 但隨著前后端分離以及分布式緩存的興起,服務(wù)端的頁面級緩存已經(jīng)很少使用了。但是在前端領(lǐng)域,頁面級緩存仍然很流行。 2. 對象緩存
2011年左右,開源中國的紅薯哥寫了很多篇關(guān)于緩存的文章。他提到:開源中國每天百萬的動態(tài)請求,只用 1 臺 4 Core 8G 的服務(wù)器就扛住了,得益于緩存框架 Ehcache。 這讓我非常神往,一個簡單的框架竟能將單機(jī)性能做到如此這般,讓我欲欲躍試。于是,我參考紅薯哥的示例代碼,在公司的余額提現(xiàn)服務(wù)上第一次使用了 Ehcache。 邏輯也很簡單,就是將成功或者失敗狀態(tài)的訂單緩存起來,這樣下次查詢的時候,不用再查詢支付寶服務(wù)了。偽代碼類似這樣: 
添加緩存之后,優(yōu)化的效果很明顯 , 任務(wù)耗時從原來的40分鐘減少到了5~10分鐘。
上面這個示例就是典型的「對象緩存」,它是本地緩存最常見的應(yīng)用場景。相比頁面緩存,它的粒度更細(xì)、更靈活,常用來緩存很少變化的數(shù)據(jù),比如:全局配置、狀態(tài)已完結(jié)的訂單等,用于提升整體的查詢速度。 3. 刷新策略
2018年,我和我的小伙伴自研了配置中心,為了讓客戶端以最快的速度讀取配置, 本地緩存使用了 Guava,整體架構(gòu)如下圖所示:

那本地緩存是如何更新的呢?有兩種機(jī)制:
- 客戶端啟動定時任務(wù),從配置中心拉取數(shù)據(jù)。
- 當(dāng)配置中心有數(shù)據(jù)變化時,主動推送給客戶端。這里我并沒有使用websocket,而是使用了 RocketMQ Remoting 通訊框架。
后來我閱讀了 Soul 網(wǎng)關(guān)的源碼,它的本地緩存更新機(jī)制如下圖所示,共支持 3 種策略: ▍zookeeper watch機(jī)制
soul-admin 在啟動的時候,會將數(shù)據(jù)全量寫入 zookeeper,后續(xù)數(shù)據(jù)發(fā)生變更時,會增量更新 zookeeper 的節(jié)點(diǎn)。與此同時,soul-web 會監(jiān)聽配置信息的節(jié)點(diǎn),一旦有信息變更時,會更新本地緩存。 ▍websocket 機(jī)制
websocket 和 zookeeper 機(jī)制有點(diǎn)類似,當(dāng)網(wǎng)關(guān)與 admin 首次建立好 websocket 連接時,admin 會推送一次全量數(shù)據(jù),后續(xù)如果配置數(shù)據(jù)發(fā)生變更,則將增量數(shù)據(jù)通過 websocket 主動推送給 soul-web。
http請求到達(dá)服務(wù)端后,并不是馬上響應(yīng),而是利用 Servlet 3.0 的異步機(jī)制響應(yīng)數(shù)據(jù)。當(dāng)配置發(fā)生變化時,服務(wù)端會挨個移除隊(duì)列中的長輪詢請求,告知是哪個 Group 的數(shù)據(jù)發(fā)生了變更,網(wǎng)關(guān)收到響應(yīng)后,再次請求該 Group 的配置數(shù)據(jù)。
長輪詢是一個有意思的話題 , 這種模式在 RocketMQ 的消費(fèi)者模型也同樣被使用,接近準(zhǔn)實(shí)時,并且可以減少服務(wù)端的壓力。 02 分布式緩存
關(guān)于分布式緩存, memcached 和 Redis 應(yīng)該是最常用的技術(shù)選型。相信程序員朋友都非常熟悉了,我這里分享兩個案例。 1.? 合理控制對象大小及讀取策略
2013年,我服務(wù)一家彩票公司,我們的比分直播模塊也用到了分布式緩存。當(dāng)時,遇到了一個 Young GC 頻繁的線上問題,通過 jstat 工具排查后,發(fā)現(xiàn)新生代每隔兩秒就被占滿了。 進(jìn)一步定位分析,原來是某些 key 緩存的 value 太大了,平均在 300K左右,最大的達(dá)到了500K。這樣在高并發(fā)下,就很容易 導(dǎo)致 GC 頻繁。 找到了根本原因后,具體怎么改呢? 我當(dāng)時也沒有清晰的思路。 于是,我去同行的網(wǎng)站上研究他們是怎么實(shí)現(xiàn)相同功能的,包括: 360彩票,澳客網(wǎng)。我發(fā)現(xiàn)了兩點(diǎn): 1、數(shù)據(jù)格式非常精簡,只返回給前端必要的數(shù)據(jù),部分?jǐn)?shù)據(jù)通過數(shù)組的方式返回
2、使用 websocket,進(jìn)入頁面后推送全量數(shù)據(jù),數(shù)據(jù)發(fā)生變化推送增量數(shù)據(jù)
再回到我的問題上,最終是用什么方案解決的呢?當(dāng)時,我們的比分直播模塊緩存格式是 JSON 數(shù)組,每個數(shù)組元素包含 20 多個鍵值對, 下面的 JSON 示例我僅僅列了其中 4 個屬性。
[{
"playId":"2399",
"guestTeamName":"小牛",
"hostTeamName":"湖人",
"europe":"123"
}]
這種數(shù)據(jù)結(jié)構(gòu),一般情況下沒有什么問題。但是當(dāng)字段數(shù)多達(dá) 20 多個,而且每天的比賽場次非常多時,在高并發(fā)的請求下其實(shí)很容易引發(fā)問題。
基于工期以及風(fēng)險考慮,最終我們采用了比較保守的優(yōu)化方案:
2)將緩存數(shù)據(jù)的格式由 JSON 改成數(shù)組,如下所示: [["2399","小牛","湖人","123"]]
修改完成之后, 緩存的大小從平均 300k 左右降為 80k 左右,YGC 頻率下降很明顯,同時頁面響應(yīng)也變快了很多。
但過了一會,cpu load 會在瞬間波動得比較高??梢姡m然我們減少了緩存大小,但是讀取大對象依然對系統(tǒng)資源是極大的損耗,導(dǎo)致 Full GC 的頻率也不低。?
3)為了徹底解決這個問題,我們使用了更精細(xì)化的緩存讀取策略。
我們把緩存拆成兩個部分,第一部分是全量數(shù)據(jù),第二部分是增量數(shù)據(jù)(數(shù)據(jù)量很?。m撁娴谝淮握埱罄∪繑?shù)據(jù),當(dāng)比分有變化的時候,通過 websocket 推送增量數(shù)據(jù)。
第 3 步完成后,頁面的訪問速度極快,服務(wù)器的資源使用也很少,優(yōu)化的效果非常優(yōu)異。
經(jīng)過這次優(yōu)化,我理解到:? 緩存雖然可以提升整體速度,但是在高并發(fā)場景下,緩存對象大小依然是需要關(guān)注的點(diǎn),稍不留神就會產(chǎn)生事故。另外我們也需要合理地控制讀取策略,最大程度減少 GC 的頻率 , 從而提升整體性能。 2.? 分頁列表查詢
列表如何緩存是我非常渴望和大家分享的技能點(diǎn)。這個知識點(diǎn)也是我 2012 年從開源中國上學(xué)到的,下面我以「查詢博客列表」的場景為例。 我們先說第 1 種方案:對分頁內(nèi)容進(jìn)行整體緩存。這種方案會 按照頁碼和每頁大小組合成一個緩存key,緩存值就是博客信息列表。 假如某一個博客內(nèi)容發(fā)生修改, 我們要重新加載緩存,或者刪除整頁的緩存。 這種方案,緩存的顆粒度比較大,如果博客更新較為頻繁,則緩存很容易失效。下面我介紹下第 2 種方案:僅對博客進(jìn)行緩存。流程大致如下: 1)先從數(shù)據(jù)庫查詢當(dāng)前頁的博客id列表,sql類似: select?id?from?blogs?limit?0,10?
2)批量從緩存中獲取博客id列表對應(yīng)的緩存數(shù)據(jù) ,并記錄沒有命中的博客id,若沒有命中的id列表大于0,再次從數(shù)據(jù)庫中查詢一次,并放入緩存,sql類似: select?id?from?blogs?where?id?in?(noHitId1,?noHitId2)
理論上,要是緩存都預(yù)熱的情況下,一次簡單的數(shù)據(jù)庫查詢,一次緩存批量獲取,即可返回所有的數(shù)據(jù)。另外,關(guān)于 緩 存批量獲取,如何實(shí)現(xiàn)? - Redis:若緩存對象結(jié)構(gòu)簡單,使用 mget 、hmget命令;若結(jié)構(gòu)復(fù)雜,可以考慮使用 pipleline,lua腳本模式
第 1 種方案適用于數(shù)據(jù)極少發(fā)生變化的場景,比如排行榜,首頁新聞資訊等。 第 2 種方案適用于大部分的分頁場景,而且能和其他資源整合在一起。舉例:在搜索系統(tǒng)里,我們可以通過篩選條件查詢出博客 id 列表,然后通過如上的方式,快速獲取博客列表。 03 多級緩存
本地緩存速度極快,但是容量有限,而且無法共享內(nèi)存。 分布式緩存容量可擴(kuò)展,但在高并發(fā)場景下,如果所有數(shù)據(jù)都必須從遠(yuǎn)程緩存種獲取,很容易導(dǎo)致帶寬跑滿,吞吐量下降。 使用多級緩存的好處在于:高并發(fā)場景下, 能提升整個系統(tǒng)的吞吐量,減少分布式緩存的壓力。 2018年,我服務(wù)的一家電商公司需要進(jìn)行 app 首頁接口的性能優(yōu)化。我花了大概兩天的時間完成了整個方案,采取的是兩級緩存模式,同時利用了 guava 的惰性加載機(jī)制,整體架構(gòu)如下圖所示: 
緩存讀取流程如下:
1、業(yè)務(wù)網(wǎng)關(guān)剛啟動時,本地緩存沒有數(shù)據(jù),讀取 Redis 緩存,如果 Redis 緩存也沒數(shù)據(jù),則通過 RPC 調(diào)用導(dǎo)購服務(wù)讀取數(shù)據(jù),然后再將數(shù)據(jù)寫入本地緩存和 Redis 中;若 Redis 緩存不為空,則將緩存數(shù)據(jù)寫入本地緩存中。
2、由于步驟1已經(jīng)對本地緩存預(yù)熱,后續(xù)請求直接讀取本地緩存,返回給用戶端。
3、Guava 配置了 refresh 機(jī)制,每隔一段時間會調(diào)用自定義 LoadingCache 線程池(5個最大線程,5個核心線程)去導(dǎo)購服務(wù)同步數(shù)據(jù)到本地緩存和 Redis 中。
優(yōu)化后,性能表現(xiàn)很好,平均耗時在 5ms 左右。最開始我以為出現(xiàn)問題的幾率很小,可是有一天晚上,突然發(fā)現(xiàn) app 端首頁顯示的數(shù)據(jù)時而相同,時而不同。
也就是說:?雖然 LoadingCache 線程一直在調(diào)用接口更新緩存信息,但是各個 服務(wù)器本地緩存中的數(shù)據(jù)并非完成一致。 說明了兩個很重要的點(diǎn): ? 1、惰性加載仍然可能造成多臺機(jī)器的數(shù)據(jù)不一致
2、 LoadingCache 線程池數(shù)量配置的不太合理,? 導(dǎo)致了線程堆積
1、惰性加載結(jié)合消息機(jī)制來更新緩存數(shù)據(jù),也就是:當(dāng)導(dǎo)購服務(wù)的配置發(fā)生變化時,通知業(yè)務(wù)網(wǎng)關(guān)重新拉取數(shù)據(jù),更新緩存。 2、適當(dāng)調(diào)大 LoadigCache 的線程池參數(shù),并在線程池埋點(diǎn),監(jiān)控線程池的使用情況,當(dāng)線程繁忙時能發(fā)出告警,然后動態(tài)修改線程池參數(shù)。 特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:



長按訂閱更多精彩▼

如有收獲,點(diǎn)個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!