轉(zhuǎn):應(yīng)用多級(jí)緩存模式支撐海量讀服務(wù)

緩存技術(shù)是一個(gè)老生常談的問題,但是它也是解決性能問題的利器,一把瑞士軍刀;而且在各種面試過程中或多或少會(huì)被問及一些緩存相關(guān)的問題,如緩存算法、熱點(diǎn)數(shù)據(jù)與更新緩存、更新緩存與原子性、緩存崩潰與快速恢復(fù)等各種與緩存相關(guān)的問題。而這些問題中有些問題又是與場景相關(guān),因此如何合理應(yīng)用緩存來解決問題也是一個(gè)選擇題。本文所有內(nèi)容是跟讀服務(wù)緩存相關(guān),不會(huì)涉及寫服務(wù)數(shù)據(jù)的緩存。本文也不考慮內(nèi)容型應(yīng)用前置的CDN架構(gòu)。本文也不會(huì)涉及緩存數(shù)據(jù)結(jié)構(gòu)優(yōu)化、緩存空間利用率跟業(yè)務(wù)數(shù)據(jù)相關(guān)的細(xì)節(jié)問題,主要從架構(gòu)和提升命中率等層面來探討緩存方案。本文將基于多級(jí)緩存模式來介紹下應(yīng)用緩存時(shí)需要注意的問題和一些解決方案,其中一些方案已經(jīng)實(shí)現(xiàn),而有一些也是想使用來解決痛點(diǎn)問題。

一、多級(jí)緩存介紹

所謂多級(jí)緩存,即在整個(gè)系統(tǒng)架構(gòu)的不同系統(tǒng)層級(jí)進(jìn)行數(shù)據(jù)緩存,以提升訪問效率,這也是應(yīng)用最廣的方案之一。我們應(yīng)用的整體架構(gòu)如下圖所示:

整體流程如上圖所示:

首先接入Nginx將請(qǐng)求負(fù)載均衡到應(yīng)用Nginx,此處常用的負(fù)載均衡算法是輪詢或者一致性哈希,輪詢可以使服務(wù)器的請(qǐng)求更加均衡,而一致性哈??梢蕴嵘龖?yīng)用Nginx的緩存命中率;后續(xù)負(fù)載均衡和緩存算法部分我們?cè)偌?xì)聊;

接著應(yīng)用Nginx讀取本地緩存(本地緩存可以使用Lua Shared Dict、Nginx Proxy Cache(磁盤/內(nèi)存)、Local Redis實(shí)現(xiàn)),如果本地緩存命中則直接返回,使用應(yīng)用Nginx本地緩存可以提升整體的吞吐量,降低后端的壓力,尤其應(yīng)對(duì)熱點(diǎn)問題非常有效;為什么要使用應(yīng)用Nginx本地緩存我們將在熱點(diǎn)數(shù)據(jù)與緩存失效部分細(xì)聊;

如果Nginx本地緩存沒命中,則會(huì)讀取相應(yīng)的分布式緩存(如Redis緩存,另外可以考慮使用主從架構(gòu)來提升性能和吞吐量),如果分布式緩存命中則直接返回相應(yīng)數(shù)據(jù)(并回寫到Nginx本地緩存);

如果分布式緩存也沒有命中,則會(huì)回源到Tomcat集群,在回源到Tomcat集群時(shí)也可以使用輪詢和一致性哈希作為負(fù)載均衡算法;

在Tomcat應(yīng)用中,首先讀取本地堆緩存,如果有則直接返回(并會(huì)寫到主Redis集群),為什么要加一層本地堆緩存將在緩存崩潰與快速修復(fù)部分細(xì)聊;

作為可選部分,如果步驟4沒有命中可以再嘗試一次讀主Redis集群操作,目的是防止當(dāng)從有問題時(shí)的流量沖擊;

如果所有緩存都沒有命中只能查詢DB或相關(guān)服務(wù)獲取相關(guān)數(shù)據(jù)并返回;

步驟7返回的數(shù)據(jù)異步寫到主Redis集群,此處可能多個(gè)Tomcat實(shí)例同時(shí)寫主Redis集群,可能造成數(shù)據(jù)錯(cuò)亂,如何解決該問題將在更新緩存與原子性部分細(xì)聊。

整體分了三部分緩存:應(yīng)用Nginx本地緩存、分布式緩存、Tomcat堆緩存,每一層緩存都用來解決相關(guān)的問題,如應(yīng)用Nginx本地緩存用來解決熱點(diǎn)緩存問題,分布式緩存用來減少訪問回源率、Tomcat堆緩存用于防止相關(guān)緩存失效/崩潰之后的沖擊。

雖然就是加緩存,但是怎么加,怎么用細(xì)想下來還是有很多問題需要權(quán)衡和考量的,接下來部分我們就詳細(xì)來討論一些緩存相關(guān)的問題。

二、如何緩存數(shù)據(jù)

2.1 過期與不過期

對(duì)于緩存的數(shù)據(jù)我們可以考慮不過期緩存和帶過期時(shí)間緩存;什么場景應(yīng)該選擇哪種模式需要根據(jù)業(yè)務(wù)和數(shù)據(jù)量等因素來決定。

不過期緩存場景一般思路如下圖所示:

如上圖所示,首先寫數(shù)據(jù)庫,如果成功則寫緩存。這種機(jī)制存在一些問題:

事務(wù)在提交時(shí)失敗則寫緩存是不會(huì)回滾的造成DB和緩存數(shù)據(jù)不一致;

假設(shè)多個(gè)人并發(fā)寫緩存可能出現(xiàn)臟數(shù)據(jù)的;

同步寫對(duì)性能有一定的影響,異步寫存在丟數(shù)據(jù)的風(fēng)險(xiǎn)。

如果對(duì)緩存數(shù)據(jù)一致性要求不是那么高,數(shù)據(jù)量也不是很大,可以考慮定期全量同步緩存。

為解決以上問題可以考慮使用消息機(jī)制,如下圖所示:

把寫緩存改成寫消息,通過消息通知數(shù)據(jù)變更;

同步緩存系統(tǒng)會(huì)訂閱消息,并根據(jù)消息進(jìn)行更新緩存;

數(shù)據(jù)一致性可以采用:消息體只包括ID、然后查庫獲取最新版本數(shù)據(jù);通過時(shí)間戳和內(nèi)容摘要機(jī)制(MD5)進(jìn)行緩存更新;

如上方法也不能保證消息不丟失,可以采用:應(yīng)用在本地記錄更新日志,當(dāng)消息丟失了回放更新日志;或者采用數(shù)據(jù)庫binlog,采用如canal訂閱binlog進(jìn)行緩存更新。

對(duì)于長尾訪問的數(shù)據(jù)、大多數(shù)數(shù)據(jù)訪問頻率都很高的場景、緩存空間足夠都可以考慮不過期緩存,比如用戶、分類、商品、價(jià)格、訂單等,當(dāng)緩存滿了可以考慮LRU機(jī)制驅(qū)逐老的緩存數(shù)據(jù)。

過期緩存機(jī)制,即采用懶加載,一般用于緩存別的系統(tǒng)的數(shù)據(jù)(無法訂閱變更消息、或者成本很高)、緩存空間有限、低頻熱點(diǎn)緩存等場景;常見步驟是:首先讀取緩存如果不命中則查詢數(shù)據(jù),然后異步寫入緩存并設(shè)置過期時(shí)間,下次讀取將命中緩存。熱點(diǎn)數(shù)據(jù)經(jīng)常使用過期緩存,即在應(yīng)用系統(tǒng)上緩存比較短的時(shí)間。這種緩存可能存在一段時(shí)間的數(shù)據(jù)不一致情況,需要根據(jù)場景來決定如何設(shè)置過期時(shí)間。如庫存數(shù)據(jù)可以在前端應(yīng)用上緩存幾秒鐘,短時(shí)間的不一致時(shí)可以忍受的。

2.2 維度化緩存與增量緩存

對(duì)于電商系統(tǒng),一個(gè)商品可能拆成如:基礎(chǔ)屬性、圖片列表、上下架、規(guī)格參數(shù)、商品介紹等;如果商品變更了要把這些數(shù)據(jù)都更新一遍那么整個(gè)更新成本很高:接口調(diào)用量和帶寬;因此最好將數(shù)據(jù)進(jìn)行維度化并增量更新(只更新變的部分)。尤其如上下架這種只是一個(gè)狀態(tài)變更,但是每天頻繁調(diào)用的,維度化后能減少服務(wù)很大的壓力。

三、分布式緩存與應(yīng)用負(fù)載均衡

3.1 緩存分布式

此處說的分布式緩存一般采用分片實(shí)現(xiàn),即將數(shù)據(jù)分散到多個(gè)實(shí)例或多臺(tái)服務(wù)器。算法一般采用取模和一致性哈希。如之前說的做不過期緩存機(jī)制可以考慮取模機(jī)制,擴(kuò)容時(shí)一般是新建一個(gè)集群;而對(duì)于可以丟失的緩存數(shù)據(jù)可以考慮一致性哈希,即使其中一個(gè)實(shí)例出問題只是丟一小部分,對(duì)于分片實(shí)現(xiàn)可以考慮客戶端實(shí)現(xiàn),或者使用如Twemproxy中間件進(jìn)行代理(分片對(duì)客戶端是透明的)。如果使用Redis可以考慮使用redis-cluster分布式集群方案。

3.2 應(yīng)用負(fù)載均衡

應(yīng)用負(fù)載均衡一般采用輪詢和一致性哈希,一致性哈希可以根據(jù)應(yīng)用請(qǐng)求的URL或者URL參數(shù)將相同的請(qǐng)求轉(zhuǎn)發(fā)到同一個(gè)節(jié)點(diǎn);而輪詢即將請(qǐng)求均勻的轉(zhuǎn)發(fā)到每個(gè)服務(wù)器;如下圖所示:

整體流程:

首先請(qǐng)求進(jìn)入接入層Nginx;

根據(jù)負(fù)載均衡算法將請(qǐng)求轉(zhuǎn)發(fā)給應(yīng)用Nginx;

如果應(yīng)用Nginx本地緩存命中,則直接返回?cái)?shù)據(jù),否則讀取分布式緩存或者回源到Tomcat。

輪詢的優(yōu)點(diǎn):到應(yīng)用Nginx的請(qǐng)求更加均勻,使得每個(gè)服務(wù)器的負(fù)載基本均衡;輪詢的缺點(diǎn):隨著應(yīng)用Nginx服務(wù)器的增加,緩存的命中率會(huì)下降,比如原來10臺(tái)服務(wù)器命中率為90%,再加10臺(tái)服務(wù)器將可能降低到45%;而這種方式不會(huì)因?yàn)闊狳c(diǎn)問題導(dǎo)致其中某一臺(tái)服務(wù)器負(fù)載過重。

一致性哈希的優(yōu)點(diǎn):相同請(qǐng)求都會(huì)轉(zhuǎn)發(fā)到同一臺(tái)服務(wù)器,命中率不會(huì)因?yàn)樵黾臃?wù)器而降低;一致性哈希的缺點(diǎn):因?yàn)橄嗤恼?qǐng)求會(huì)轉(zhuǎn)發(fā)到同一臺(tái)服務(wù)器,因此可能造成某臺(tái)服務(wù)器負(fù)載過重,甚至因?yàn)檎?qǐng)求太多導(dǎo)致服務(wù)出現(xiàn)問題。

解決辦法是根據(jù)實(shí)際情況動(dòng)態(tài)選擇使用哪種算法:

負(fù)載較低時(shí)使用一致性哈希;

熱點(diǎn)請(qǐng)求降級(jí)一致性哈希為輪詢;

將熱點(diǎn)數(shù)據(jù)推送到接入層Nginx,直接響應(yīng)給用戶。

四、熱點(diǎn)數(shù)據(jù)與更新緩存

熱點(diǎn)數(shù)據(jù)會(huì)造成服務(wù)器壓力過大,導(dǎo)致服務(wù)器性能、吞吐量、帶寬達(dá)到極限,出現(xiàn)響應(yīng)慢或者拒絕服務(wù)的情況,這肯定是不允許的??梢詮娜缦聨讉€(gè)方案去解決。

4.1 單機(jī)全量緩存+主從

如上圖所示,所有緩存都存儲(chǔ)在應(yīng)用本機(jī),回源之后會(huì)把數(shù)據(jù)更新到主Redis集群,然后通過主從復(fù)制到其他從Redis集群。緩存的更新可以采用懶加載或者訂閱消息進(jìn)行同步。

4.2 分布式緩存+應(yīng)用本地?zé)狳c(diǎn)

對(duì)于分布式緩存,我們需要在Nginx+Lua應(yīng)用中進(jìn)行應(yīng)用緩存來減少Redis集群的訪問沖擊;即首先查詢應(yīng)用本地緩存,如果命中則直接緩存,如果沒有命中則接著查詢Redis集群、回源到Tomcat;然后將數(shù)據(jù)緩存到應(yīng)用本地。

此處到應(yīng)用Nginx的負(fù)載機(jī)制采用:正常情況采用一致性哈希,如果某個(gè)請(qǐng)求類型訪問量突破了一定的閥值,則自動(dòng)降級(jí)為輪詢機(jī)制。另外對(duì)于一些秒殺活動(dòng)之類的熱點(diǎn)我們是可以提前知道的,可以把相關(guān)數(shù)據(jù)預(yù)先推送到應(yīng)用Nginx并將負(fù)載均衡機(jī)制降級(jí)為輪詢。

另外可以考慮建立實(shí)時(shí)熱點(diǎn)發(fā)現(xiàn)系統(tǒng)來發(fā)現(xiàn)熱點(diǎn):

接入Nginx將請(qǐng)求轉(zhuǎn)發(fā)給應(yīng)用Nginx;

應(yīng)用Nginx首先讀取本地緩存;如果命中直接返回,不命中會(huì)讀取分布式緩存、回源到Tomcat進(jìn)行處理;

應(yīng)用Nginx會(huì)將請(qǐng)求上報(bào)給實(shí)時(shí)熱點(diǎn)發(fā)現(xiàn)系統(tǒng),如使用UDP直接上報(bào)請(qǐng)求、或者將請(qǐng)求寫到本地kafka、或者使用flume訂閱本地nginx日志;上報(bào)給實(shí)時(shí)熱點(diǎn)發(fā)現(xiàn)系統(tǒng)后,它將進(jìn)行統(tǒng)計(jì)熱點(diǎn)(可以考慮storm實(shí)時(shí)計(jì)算);

根據(jù)設(shè)置的閥值將熱點(diǎn)數(shù)據(jù)推送到應(yīng)用Nginx本地緩存。

因?yàn)樽隽吮镜鼐彺?,因此?duì)于數(shù)據(jù)一致性需要我們?nèi)タ紤],即何時(shí)失效或更新緩存:

如果可以訂閱數(shù)據(jù)變更消息,那么可以訂閱變更消息進(jìn)行緩存更新;

如果無法訂閱消息或者訂閱消息成本比較高,并且對(duì)短暫的數(shù)據(jù)一致性要求不嚴(yán)格(比如在商品詳情頁看到的庫存,可以短暫的不一致,只要保證下單時(shí)一致即可),那么可以設(shè)置合理的過期時(shí)間,過期后再查詢新的數(shù)據(jù);

如果是秒殺之類的,可以訂閱活動(dòng)開啟消息,將相關(guān)數(shù)據(jù)提前推送到前端應(yīng)用,并將負(fù)載均衡機(jī)制降級(jí)為輪詢;

建立實(shí)時(shí)熱點(diǎn)發(fā)現(xiàn)系統(tǒng)來對(duì)熱點(diǎn)進(jìn)行統(tǒng)一推送和更新。

五、更新緩存與原子性

正如之前說的如果多個(gè)應(yīng)用同時(shí)操作一份數(shù)據(jù)很可能造成緩存數(shù)據(jù)是臟數(shù)據(jù),解決辦法:

1.1 更新數(shù)據(jù)時(shí)使用更新時(shí)間戳或者版本對(duì)比,如果使用Redis可以利用其單線程機(jī)制進(jìn)行原子化更新;

1.2 使用如canal訂閱數(shù)據(jù)庫binlog;

2.1 將更新請(qǐng)求按照相應(yīng)的規(guī)則分散到多個(gè)隊(duì)列,然后每個(gè)隊(duì)列的進(jìn)行單線程更新,更新時(shí)拉取最新的數(shù)據(jù)保存;

2.2 分布式鎖,更新之前獲取相關(guān)的鎖。

六、緩存崩潰與快速修復(fù)

6.1 取模

對(duì)于取模機(jī)制如果其中一個(gè)實(shí)例壞了,如果摘除此實(shí)例將導(dǎo)致大量緩存不命中,瞬間大流量可能導(dǎo)致后端DB/服務(wù)出現(xiàn)問題。對(duì)于這種情況可以采用主從機(jī)制來避免實(shí)例壞了的問題,即其中一個(gè)實(shí)例壞了可以那從/主頂上來。但是取模機(jī)制下如果增加一個(gè)節(jié)點(diǎn)將導(dǎo)致大量緩存不命中,一般是建立另一個(gè)集群,然后把數(shù)據(jù)遷移到新集群,然后把流量遷移過去。

6.2 一致性哈希

對(duì)于一致性哈希機(jī)制如果其中一個(gè)實(shí)例壞了,如果摘除此實(shí)例將只影響一致性哈希環(huán)上的部分緩存不命中,不會(huì)導(dǎo)致瞬間大量回源到后端DB/服務(wù),但是也會(huì)產(chǎn)生一些影響。

另外也可能因?yàn)橐恍┱`操作導(dǎo)致整個(gè)緩存集群出現(xiàn)了問題,如何快速恢復(fù)呢?

6.3 快速恢復(fù)

如果出現(xiàn)之前說到的一些問題,可以考慮如下方案:

主從機(jī)制,做好冗余,即其中一部分不可用,將對(duì)等的部分補(bǔ)上去;

如果因?yàn)榫彺鎸?dǎo)致應(yīng)用可用性已經(jīng)下降可以考慮:1、部分用戶降級(jí),然后慢慢減少降級(jí)量;2、后臺(tái)通過Worker預(yù)熱緩存數(shù)據(jù)。

也就是如果整個(gè)緩存集群壞了,而且沒有備份,那么只能去慢慢將緩存重建;為了讓部分用戶還是可用的,可以根據(jù)系統(tǒng)承受能力,通過降級(jí)方案讓一部分用戶先用起來,將這些用戶相關(guān)的緩存重建;另外通過后臺(tái)Worker進(jìn)行緩存數(shù)據(jù)的預(yù)熱。

作者:張開濤(微信號(hào):kaitao-1234567),京東資深Java工程師,2014年加入京東,主要負(fù)責(zé)商品詳情頁、詳情頁統(tǒng)一服務(wù)架構(gòu)與開發(fā)工作,設(shè)計(jì)并開發(fā)了多個(gè)億級(jí)訪問量系統(tǒng)。工作之余喜歡寫技術(shù)博客,有《跟我學(xué)spring》、《跟我學(xué)Spring MVC》、《跟我學(xué)Shiro》、《跟我學(xué)Nginx+Lua開發(fā)》等系列教程,目前博客訪問量有460萬+

聲明:本文為作者原創(chuàng)文章投稿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容