首先我們需要明白,什么是一個多級緩存系統(tǒng),它有什么用。所謂多級緩存系統(tǒng),就是指在一個系統(tǒng) 的不同的架構(gòu)層級進(jìn)行數(shù)據(jù)緩存,以提升訪問效率。
我們都知道,一個緩存系統(tǒng),它面臨著許多問題,比如緩存擊穿,緩存穿透,緩存雪崩,緩存熱點(diǎn)等等問題,那么,對于一個多級緩存系統(tǒng),它有什么問題呢?
緩存熱點(diǎn):多級緩存系統(tǒng)大多應(yīng)用在高并發(fā)場景下,所以我們需要解決熱點(diǎn)Key問題,如何探測熱點(diǎn)key?
數(shù)據(jù)一致性:各層緩存之間的數(shù)據(jù)一致性問題,如應(yīng)用層緩存和分布式緩存之前的數(shù)據(jù)一致性問題。
緩存過期:緩存數(shù)據(jù)可以分為兩大類,過期緩存和不過期緩存?如何設(shè)計,如何設(shè)計過期緩存?
在這之前,我們先看看一個簡單的多級緩存系統(tǒng)的架構(gòu)圖:
整個多級緩存系統(tǒng)被分為三層,應(yīng)用層Nginx緩存,分布式redis緩存集群,Tomcat堆內(nèi)緩存。整個架構(gòu)流程如下:
當(dāng)接收到一個請求時,首先會分發(fā)到nginx集群中,這里可以采用nginx的負(fù)載均衡算法分發(fā)給某一臺機(jī)器,使用輪詢可以降低負(fù)載,或者采用一致性hash算法來提升緩存命中率。
當(dāng)nginx層沒有緩存數(shù)據(jù)時,會繼續(xù)向下請求,在分布式緩存集群中查找數(shù)據(jù),如果緩存命中,直接返回(并且寫入nginx應(yīng)用緩存中),如果未命中,則回源到tomcat集群中查詢堆內(nèi)緩存。
在分布式緩存中查詢不到數(shù)據(jù),將會去tomcat集群中查詢堆內(nèi)緩存,查詢成功直接返回(并寫入分redis主集群中),查詢失敗請求數(shù)據(jù)庫;堆內(nèi)緩存。
如果以上緩存中都沒有命中,則直接請求數(shù)據(jù)庫,返回結(jié)果,同步數(shù)據(jù)到分布式緩存中。
在簡單了解了多級緩存的基本架構(gòu)之后,我們就該思考如何解決上面提到的一系列問題。
緩存熱點(diǎn)
緩存熱點(diǎn),是一個很常見的問題,比如“某某明星宣布結(jié)婚”等等,都可能產(chǎn)生大量請求訪問的問題,一個最麻煩也是最容易讓人忽視的事情就是如何探測到熱點(diǎn)key,在緩存系統(tǒng)中,除了一些常用的熱點(diǎn)key外,在某些特殊場合下也會出現(xiàn)大量的熱點(diǎn)key,我們該如何發(fā)現(xiàn)呢?有以下策略:
數(shù)據(jù)調(diào)研。可以分析歷史數(shù)據(jù)以及針對不同的場合去預(yù)測出熱點(diǎn)key,這種方式雖然不能百分百使得緩存命中,但是卻是一種最簡單和節(jié)省成本的方案。
實(shí)時計算。可以使用現(xiàn)有的實(shí)時計算框架,比如storm、spark streaming、flink等框架統(tǒng)計一個時間段內(nèi)的請求量,從而判斷熱點(diǎn)key。或者也可以自己實(shí)現(xiàn)定時任務(wù)去統(tǒng)計請求量。
這里我們著重討論一下第二種解決方案,對于熱點(diǎn)key問題,當(dāng)緩存系統(tǒng)中沒有發(fā)現(xiàn)緩存時,需要去數(shù)據(jù)庫中讀取數(shù)據(jù),當(dāng)大量請求來的時候,一個請求獲取鎖去請求數(shù)據(jù)庫,其他阻塞,接著全部去訪問緩存,這樣可能因?yàn)橐慌_服務(wù)器撐不住從而宕機(jī),比如正常一臺服務(wù)器并發(fā)量為5w左右,產(chǎn)生熱點(diǎn)key的時候達(dá)到了10w甚至20w,這樣服務(wù)器肯定會崩。所以我們在發(fā)現(xiàn)熱點(diǎn)key之后還需要做到如何自動負(fù)載均衡。
結(jié)合以上問題我們重新設(shè)計架構(gòu),如下圖所示:
我們將整個應(yīng)用架構(gòu)分為應(yīng)用層,分布式緩存、系統(tǒng)層以及數(shù)據(jù)層。
在應(yīng)用層,我們采用nginx集群,并且對接實(shí)時計算鏈路,通過flume監(jiān)控nginx日志,將數(shù)據(jù)傳輸?shù)絢afka集群中,然后flink集群消費(fèi)數(shù)據(jù)進(jìn)行統(tǒng)計,如果統(tǒng)計 結(jié)果為熱點(diǎn)key,則將數(shù)據(jù)寫入zookeeper的節(jié)點(diǎn)中,而應(yīng)用系統(tǒng)通過監(jiān)控znode節(jié)點(diǎn),讀取熱點(diǎn)key數(shù)據(jù),去數(shù)據(jù)庫中加載數(shù)據(jù)到緩存中并且做到負(fù)載均衡。
實(shí)際上,對于應(yīng)用系統(tǒng)中的每一臺服務(wù)器,還需要一層防護(hù)機(jī)制,限流熔斷,這樣做的目的是為了防止單臺機(jī)器請求量過高,使得服務(wù)器負(fù)載過高,不至于服務(wù)器宕機(jī)或者大量請求訪問數(shù)據(jù)庫。簡單思路就是為每一臺服務(wù)器設(shè)計一個閥值,當(dāng)請求量大于該值就直接返回用戶空白頁面或者提示用戶幾秒后刷新重新訪問。
數(shù)據(jù)一致性
數(shù)據(jù)一致性問題主要體現(xiàn)在緩存更新的時候,如何更新緩存,保證數(shù)據(jù)庫與緩存以及各層緩存層之間的一致性。
對于緩存更新問題,先寫緩存還是先寫數(shù)據(jù)庫,這里省略若干字。之前的文章介紹過,有興趣的讀者可以翻閱。
在單層緩存系統(tǒng)中,我們可以先刪除緩存然后更新數(shù)據(jù)庫的方案來解決其數(shù)據(jù)一致性問題,那么對于多級緩存呢?如果使用這種方案,我們需要考慮,如果先刪除緩存,那么需要逐層去做刪除操作,那么這一系列操作對系統(tǒng)帶來的耗時也是和可觀的。
如果我們使用分布式事務(wù)機(jī)制,就需要考慮該不該將寫緩存放入事務(wù)當(dāng)中,因?yàn)槲覀兏路植际骄彺妫枰呔W(wǎng)絡(luò)通信,大量的請求將導(dǎo)致網(wǎng)路抖動甚至阻塞,增加了系統(tǒng)的延遲,導(dǎo)致系統(tǒng)短時間內(nèi)不可用。如果我們不將寫緩存這一操作放入事務(wù)當(dāng)中,那么可能引起短時間內(nèi)數(shù)據(jù)不一致。這也就是分布式系統(tǒng)的CAP理論,我們不能同時達(dá)到高可用和一致性。那么該如何抉擇呢?
這里我們選擇保證系統(tǒng)的可用性,就一個秒殺系統(tǒng)來講,短暫的不一致性問題對用戶的體驗(yàn)影響并不大(當(dāng)然,這里不涉及支付系統(tǒng)),而可用性對用戶來說卻很重要,一個活動可能在很短的時間內(nèi)結(jié)束,而用戶需要在這段時間內(nèi)搶到自己心儀的商品,所以可用性更重要一些(這里需要根據(jù)具體場景進(jìn)行權(quán)衡)。
在保證了系統(tǒng)的可用性的基礎(chǔ)上,我們該如何實(shí)現(xiàn)呢?如果實(shí)時性要求不是很高,我們可以采用全量+增量同步的方式進(jìn)行。首先,我們可以按照預(yù)計的熱點(diǎn)key對系統(tǒng)進(jìn)行緩存預(yù)熱,全量同步數(shù)據(jù)到緩存系統(tǒng)。接著,在需要更新緩存的時候,我們可以采用增量同步的方式更新緩存。比如我們可以使用阿里Canal框架同步binlog的方式進(jìn)行數(shù)據(jù)的同步。
緩存過期
緩存系統(tǒng)中的所有數(shù)據(jù),根據(jù)數(shù)據(jù)的使用頻率以及場景,我們可以分為過期key以及不過期key,那么對齊過期緩存我們該如何淘汰呢?下面有常用的幾種方案:
FIFO:使用FIFO算法來淘汰過期緩存。
LFU:使用LFU算法來淘汰過期緩存。
LRU:使用LRU算法來淘汰過期緩存。
以上幾種方案是在緩存達(dá)到最大緩存大小的時候的淘汰策略,如果沒有達(dá)到最大緩存大小,我們有下面幾種方式:
定時刪除策略:設(shè)置一個定時任務(wù),在規(guī)定時間內(nèi)檢查并且刪除過期key。
定期刪除策略:這種策略需要設(shè)置刪除的周期以及時長,如何設(shè)置,需要根據(jù)具體場合來計算。
惰性刪除策略:在使用時檢查是否過期,如果過期直接去更新緩存,否則直接返回。