這個問題很微妙,可能這位同學內(nèi)心深處,覺得 Redis 是所有應用緩存的標配。 緩存的世界很廣闊,對于應用系統(tǒng)來講,我們經(jīng)常將緩存劃分為本地緩存和分布式緩存。 本地緩存?:應用中的緩存組件,緩存組件和應用在同一進程中,緩存的讀寫非???,沒有網(wǎng)絡(luò)開銷。但各應用或集群的各節(jié)點都需要維護自己的單獨緩存,無法共享緩存。 分布式緩存:和應用分離的緩存組件或服務(wù),與本地應用隔離,多個應用可直接共享緩存。
1 緩存的本質(zhì)
我們常常會講:“加了緩存,我們的系統(tǒng)就會更快” 。 所謂的 “更快”,本質(zhì)上做到了如下兩點:
減小 CPU 消耗
將原來需要實時計算的內(nèi)容提前算好、把一些公用的數(shù)據(jù)進行復用,這可以減少 CPU 消耗,從而提升響應性能。
減小 I/O 消耗
將原來對網(wǎng)絡(luò)、磁盤等較慢介質(zhì)的讀寫訪問變?yōu)閷?nèi)存等較快介質(zhì)的訪問,從而提升響應性能。
假如可以通過增強 CPU、I/O 本身的性能來滿足需求的話,升級硬件往往是更好的解決方案,即使需要一些額外的投入成本,也通常要優(yōu)于引入緩存后可能帶來的風險。 從開發(fā)角度來說,引入緩存會提高系統(tǒng)復雜度,因為你要考慮緩存的失效、更新、一致性等問題。 從運維角度來說,緩存會掩蓋掉一些缺陷,讓問題在更久的時間以后,出現(xiàn)在距離發(fā)生現(xiàn)場更遠的位置上。 從安全角度來說,緩存可能泄漏某些保密數(shù)據(jù),也是容易受到攻擊的薄弱點。 因此,緩存是把雙刃劍。
2 本地緩存 JDK Map
JDK Map 經(jīng)常用于緩存實現(xiàn):
HashMap
HashMap 是一種基于哈希表的集合類,它提供了快速的插入、查找和刪除操作??梢詫㈡I值對作為緩存項的存儲方式,將鍵作為緩存項的唯一標識符,值作為緩存項的內(nèi)容。
ConcurrentHashMap
ConcurrentHashMap 是線程安全的 HashMap,它在多線程環(huán)境下可以保證高效的并發(fā)讀寫操作。
LinkedHashMap
LinkedHashMap 是一種有序的 HashMap ,它保留了元素插入的順序,可以按照插入順序或者訪問順序進行遍歷。
TreeMap
TreeMap 是一種基于紅黑樹的有序 Map,它可以按照鍵的順序進行遍歷。
筆者曾經(jīng)負責藝龍紅包系統(tǒng),紅包活動就是存儲在?ConcurrentHashMap?中 ,通過定時任務(wù)刷新緩存?。
核心流程: 1、紅包系統(tǒng)啟動后,初始化一個 ConcurrentHashMap 作為紅包活動緩存 ; 2、數(shù)據(jù)庫查詢所有的紅包活動,并將活動信息存儲在 Map 中; 3、定時任務(wù)每隔 30 秒 ,執(zhí)行緩存加載方法,刷新緩存。 為什么紅包系統(tǒng)會將紅包活動信息存儲在本地內(nèi)存 ConcurrentHashMap 呢 ?
紅包系統(tǒng)是高并發(fā)應用,快速將請求結(jié)果響應給前端,大大提升用戶體驗;
紅包活動數(shù)量并不多,就算全部放入到 Map 里也不會產(chǎn)生內(nèi)存溢出的問題;
定時任務(wù)刷新緩存并不會影響紅包系統(tǒng)的業(yè)務(wù)。
筆者見過很多單體應用都使用這種方案,該方案的特點是簡潔易用,工程實現(xiàn)也容易 。
3 本地緩存框架
雖然使用 JDK Map 能快捷構(gòu)建緩存,但緩存的功能還是比較孱弱的。 因為現(xiàn)實場景里,我們可能需要給緩存添加緩存統(tǒng)計、過期失效、淘汰策略等功能。 于是,本地緩存框架應運而生。 流行的 Java 緩存框架包括:Ehcache , Google Guava , Caffine Cache 。
下圖展示了 Caffine 框架的使用示例。
雖然本地緩存框架的功能很強大,但是本地緩存的缺陷依然明顯。 1、高并發(fā)的場景,應用重啟之后,本地緩存就失效了,系統(tǒng)的負載就比較大,需要花較長的時間才能恢復; 2、每個應用節(jié)點都會維護自己的單獨緩存,緩存同步比較頭疼。
4 分布式緩存
分布式緩存是指將緩存數(shù)據(jù)分布在多臺機器上,以提高緩存容量和并發(fā)讀寫能力的緩存系統(tǒng)。分布式緩存通常由多臺機器組成一個集群,每臺機器上都運行著相同的緩存服務(wù)進程,緩存數(shù)據(jù)被均勻地分布在集群中的各個節(jié)點上。 Redis 是分布式緩存的首選,甚至我們一提到緩存,很多后端工程師首先想到的就它。 下圖是神州專車訂單的 Redis 集群架構(gòu) 。將 Redis 集群拆分成四個分片,每個分片包含一主一從,主從可以切換。應用 A 根據(jù)不同的緩存 key 訪問不同的分片。
與本地緩存相比,分布式緩存具有以下優(yōu)點: 1、容量和性能可擴展 通過增加集群中的機器數(shù)量,可以擴展緩存的容量和并發(fā)讀寫能力。同時,緩存數(shù)據(jù)對于應用來講都是共享的。 2、高可用性 由于數(shù)據(jù)被分布在多臺機器上,即使其中一臺機器故障,緩存服務(wù)也能繼續(xù)提供服務(wù)。 但是分布式緩存的缺點同樣不容忽視。 1、網(wǎng)絡(luò)延遲 分布式緩存通常需要通過網(wǎng)絡(luò)通信來進行數(shù)據(jù)讀寫,可能會出現(xiàn)網(wǎng)絡(luò)延遲等問題,相對于本地緩存而言,響應時間更長。 2、復雜性 分布式緩存需要考慮序列化、數(shù)據(jù)分片、緩存大小等問題,相對于本地緩存而言更加復雜。 筆者曾經(jīng)也認為無腦上緩存 ,系統(tǒng)就一定更快,但直到一次事故,對于分布式緩存的觀念才徹底改變。 2014 年,同事開發(fā)了比分直播的系統(tǒng),所有的請求都是從分布式緩存 Memcached 中獲取后直接響應。常規(guī)情況下,從緩存中查詢數(shù)據(jù)非???,但在線用戶稍微多一點,整個系統(tǒng)就會特別卡。 通過 jstat 命令發(fā)現(xiàn) GC 頻率極高,幾次請求就將新生代占滿了,而且 CPU 的消耗都在 GC 線程上。初步判斷是緩存值過大導致的,果不其然,緩存大小在 300k 到 500k 左右。 解決過程還比較波折,分為兩個步驟:
修改新生代大小,從原來的 2G 修改成 4G,并精簡緩存數(shù)據(jù)大小 (從平均 300k 左右降為 80k 左右);
把緩存拆成兩個部分,第一部分是全量數(shù)據(jù),第二部分是增量數(shù)據(jù)(數(shù)據(jù)量很?。?。頁面第一次請求拉取全量數(shù)據(jù),當比分有變化的時候,通過 websocket 推送增量數(shù)據(jù)。
經(jīng)過這次優(yōu)化,筆者理解到:緩存雖然可以提升整體速度,但是在高并發(fā)場景下,緩存對象大小依然是需要關(guān)注的點,稍不留神就會產(chǎn)生事故。另外我們也需要合理地控制讀取策略,最大程度減少 GC 的頻率,從而提升整體性能。
5 多級緩存
開源中國網(wǎng)站最開始完全是用本地緩存框架 Ehcache 。 后來隨著訪問量的激增,出現(xiàn)了一個可怕的問題:“因為 Java 程序更新很頻繁,每次更新的時候都要重啟。一旦重啟后,整個 Ehcache 緩存里的數(shù)據(jù)都被清掉。重啟后若大量訪問進來的話,開源中國的數(shù)據(jù)庫基本上很快就會崩掉”。 于是,開源中國開發(fā)了多級緩存框架?J2Cache,使用了多級緩存?Ehcache + Redis?。 多級緩存有如下優(yōu)勢:
離用戶越近,速度越快;
減少分布式緩存查詢頻率,降低序列化和反序列化的 CPU 消耗;
大幅度減少網(wǎng)絡(luò) IO 以及帶寬消耗。
本地緩存做為一級緩存,分布式緩存做為二級緩存,首先從一級緩存中查詢,若能查詢到數(shù)據(jù)則直接返回,否則從二級緩存中查詢,若二級緩存中可以查詢到數(shù)據(jù),則回填到一級緩存中,并返回數(shù)據(jù)。若二級緩存也查詢不到,則從數(shù)據(jù)源中查詢,將結(jié)果分別回填到一級緩存,二級緩存中。
2018 年,筆者服務(wù)的一家電商公司需要進行 app 首頁接口的性能優(yōu)化。筆者花了大概兩天的時間完成了整個方案,采取的是兩級緩存模式,同時利用了 Guava 的惰性加載機制,整體架構(gòu)如下圖所示:
緩存讀取流程如下: 1、業(yè)務(wù)網(wǎng)關(guān)剛啟動時,本地緩存沒有數(shù)據(jù),讀取 Redis 緩存,如果 Redis 緩存也沒數(shù)據(jù),則通過 RPC 調(diào)用導購服務(wù)讀取數(shù)據(jù),然后再將數(shù)據(jù)寫入本地緩存和 Redis 中;若 Redis 緩存不為空,則將緩存數(shù)據(jù)寫入本地緩存中。 2、由于步驟 1 已經(jīng)對本地緩存預熱,后續(xù)請求直接讀取本地緩存,返回給用戶端。 3、Guava 配置了 refresh 機制,每隔一段時間會調(diào)用自定義 LoadingCache 線程池(5 個最大線程,5 個核心線程)去導購服務(wù)同步數(shù)據(jù)到本地緩存和 Redis 中。 優(yōu)化后,性能表現(xiàn)很好,平均耗時在 5ms 左右。最開始我以為出現(xiàn)問題的幾率很小,可是有一天晚上,突然發(fā)現(xiàn) app 端首頁顯示的數(shù)據(jù)時而相同,時而不同。 也就是說:雖然 LoadingCache 線程一直在調(diào)用接口更新緩存信息,但是各個 服務(wù)器本地緩存中的數(shù)據(jù)并非完成一致。說明了兩個很重要的點: 1、惰性加載仍然可能造成多臺機器的數(shù)據(jù)不一致 2、LoadingCache 線程池數(shù)量配置的不太合理,導致了線程堆積 最終,我們的解決方案是: 1、惰性加載結(jié)合消息機制來更新緩存數(shù)據(jù),也就是:當導購服務(wù)的配置發(fā)生變化時,通知業(yè)務(wù)網(wǎng)關(guān)重新拉取數(shù)據(jù),更新緩存。 2、適當調(diào)大 LoadigCache 的線程池參數(shù),并在線程池埋點,監(jiān)控線程池的使用情況,當線程繁忙時能發(fā)出告警,然后動態(tài)修改線程池參數(shù)。
6 沒有銀彈
沒有銀彈是 Fred Brooks 在 1987 年所發(fā)表的一篇關(guān)于軟件工程的經(jīng)典論文。 論文強調(diào)真正的銀彈并不存在,而所謂的銀彈則是指沒有任何一項技術(shù)或方法可以能讓軟件工程的生產(chǎn)力在十年內(nèi)提高十倍。 通俗來講:在技術(shù)領(lǐng)域中沒有一種通用的解決方案可以解決所有問題。 技術(shù)本質(zhì)上是為了解決問題而存在的,每個問題都有其獨特的環(huán)境和限制條件,沒有一種通用的技術(shù)或工具可以完美地解決所有問題。 雖然技術(shù)不斷發(fā)展和進步,但是對于復雜的問題,仍需要結(jié)合多種技術(shù)和方法,進行系統(tǒng)性的思考和綜合性的解決方案設(shè)計,才能得到最優(yōu)解決方案。 回到文章開頭的問題 ,如何說服技術(shù)老大用 Redis ? 假如應用就是一個單體應用,緩存可以不共享,通過定時任務(wù)刷新緩存對業(yè)務(wù)沒有影響,而且本地內(nèi)存可以 Hold 住緩存的對象大小,那么你的技術(shù)老大的方案沒有問題。 假如應用業(yè)務(wù)比較復雜,需要使用緩存提升系統(tǒng)的性能,同時分布式緩存共享的特性對于研發(fā)來講開發(fā)更加快捷,Redis 確實是個不錯的選擇,可以從研發(fā)成本、代碼維護、人力模型等多個角度和技術(shù)老大提出自己的觀點。 總而言之,在技術(shù)領(lǐng)域中,沒有銀彈。我們需要不斷探索和研究新的技術(shù),但同時也需要認識到技術(shù)的局限性,不盲目追求所謂的 “銀彈”,而是結(jié)合具體問題和需求,選擇最適合的解決方案。
?
編輯:黃飛
?
評論
查看更多