如何正確的使用緩存

1. 常見(jiàn)概念

在合理應(yīng)用緩存前,需要了解緩存領(lǐng)域里相關(guān)的幾個(gè)常用術(shù)語(yǔ):

1)緩存命中:表示數(shù)據(jù)能夠從緩存中獲取,不需要回源;

2)Cache miss:表示沒(méi)有命中緩存,如果緩存內(nèi)存中還有內(nèi)存空間的話,會(huì)將數(shù)據(jù)加入到緩存中;

3)存儲(chǔ)成本:當(dāng)沒(méi)有命中緩存時(shí),回源獲取后會(huì)將數(shù)據(jù)放置到存儲(chǔ)中,整個(gè)將數(shù)據(jù)放置到存儲(chǔ)空間所需要的時(shí)間以及空間稱(chēng)之為存儲(chǔ)成本;

4)緩存失效:當(dāng)源數(shù)據(jù)發(fā)生變更后,意味著緩存中的數(shù)據(jù)失效;

5)緩存污染:將不經(jīng)常訪問(wèn)的數(shù)據(jù)放置到緩存存儲(chǔ)空間中,以至于高頻訪問(wèn)的數(shù)據(jù)無(wú)法放置到緩存中;

6)替代策略:當(dāng)數(shù)據(jù)放置到緩存空間時(shí),由于空間不足時(shí),就需要從緩存空間中去除已有的數(shù)據(jù),選擇去除哪些數(shù)據(jù)就是由替代策略決定的。常見(jiàn)的替代策略有如下這些:

Least-Recently-Used(LRU)

Least-Frequently-Used(LFU)

SIZE

First in First Out(FIFO)

由于存儲(chǔ)空間有限,替代策略要解決的核心問(wèn)題是盡量保留高頻訪問(wèn)的緩存數(shù)據(jù),降低緩存污染以提升緩存命中率和整體的緩存效率,難點(diǎn)在于,需要基于數(shù)據(jù)歷史訪問(wèn)情況,以一種合適的對(duì)未來(lái)訪問(wèn)情況的預(yù)估才能找到更佳的策略。

2. 訪問(wèn)緩存場(chǎng)景分析

使用緩存通常的操作是,請(qǐng)求先訪問(wèn)緩存數(shù)據(jù),如果緩存中不存在的話,就會(huì)回源到數(shù)據(jù)庫(kù)中然后將數(shù)據(jù)寫(xiě)入到緩存中;如果存在的話就直接返回?cái)?shù)據(jù)。從整個(gè)過(guò)程來(lái)看,緩存層就處于數(shù)據(jù)訪問(wèn)的前置環(huán)節(jié),分擔(dān)數(shù)據(jù)庫(kù)在高并發(fā)容易出現(xiàn)系統(tǒng)故障的風(fēng)險(xiǎn),所以在使用過(guò)程中需要對(duì)緩存層很謹(jǐn)慎的進(jìn)行分析。在訪問(wèn)緩存數(shù)據(jù)時(shí),有常見(jiàn)的三大場(chǎng)景:緩存穿透、緩存擊穿以及緩存雪崩。

2.1 緩存穿透

現(xiàn)象:每次請(qǐng)求直接穿透緩存層,直接回源到數(shù)據(jù)庫(kù),給數(shù)據(jù)庫(kù)帶來(lái)比較大的訪問(wèn)量,甚至宕機(jī)。

原因:緩存中不存的的數(shù)據(jù),直接查詢(xún)數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)也不存在,也就是說(shuō)請(qǐng)求的數(shù)據(jù)永遠(yuǎn)不會(huì)寫(xiě)入緩存,這就導(dǎo)致每次請(qǐng)求都會(huì)打到db,造成數(shù)據(jù)庫(kù)壓力過(guò)大。

解決方法:

方案一:保存空對(duì)象在緩存中,設(shè)置較短的過(guò)期時(shí)間;

方案二:業(yè)務(wù)上過(guò)濾非法參數(shù),防止非法請(qǐng)求擊垮數(shù)據(jù)庫(kù);

方案三:采用bloom filter保存緩存過(guò)的key,在訪問(wèn)請(qǐng)求到來(lái)時(shí)可以過(guò)濾掉不存在的key,防止這些請(qǐng)求到db層;(這個(gè)暫時(shí)沒(méi)有用過(guò))

2.2 緩存擊穿

現(xiàn)象:熱點(diǎn)key 失效時(shí),大量的請(qǐng)求穿過(guò)緩存,直接訪問(wèn)db,對(duì)數(shù)據(jù)庫(kù)造成很大的壓力;

原因:一般的緩存key都會(huì)有過(guò)期時(shí)間,當(dāng)key 失效的時(shí)候,高并發(fā)情況下,熱點(diǎn)key會(huì)有大量的請(qǐng)求直接打到db 上,對(duì)數(shù)據(jù)庫(kù)造成非常大的壓力;

解決方法:

方案一:使用互斥鎖,當(dāng)緩存失效時(shí),保證只有一個(gè)請(qǐng)求能夠訪問(wèn)db,進(jìn)行更新緩存操作,其他請(qǐng)求等待重試;

方案二:緩存"永不失效",異步更新緩存,更新時(shí)間設(shè)置在key 過(guò)期之前;

2.3 緩存雪崩

現(xiàn)象:同時(shí)間大量key同時(shí)失效,大量請(qǐng)求直接訪問(wèn)db,對(duì)數(shù)據(jù)庫(kù)造成很大壓力;

原因:緩存key 的過(guò)期時(shí)間設(shè)置比較集中,同時(shí)失效,大量請(qǐng)求直接訪問(wèn)到db,最終導(dǎo)致數(shù)據(jù)庫(kù)瞬時(shí)壓力過(guò)大而崩潰、

解決方案

方案一:為redis key 設(shè)置失效時(shí)間的時(shí)候盡量分散,可以在固定超時(shí)時(shí)間浮動(dòng)幾分鐘,保證不會(huì)出現(xiàn)大規(guī)模key 失效的情況;

方案二:同緩存擊穿,設(shè)置互斥鎖

2.4 總結(jié)

緩存穿透、緩存擊穿以及緩存雪崩這三個(gè)術(shù)語(yǔ)很容易弄混,也是讀緩存中典型的三個(gè)場(chǎng)景問(wèn)題,做一下簡(jiǎn)單的總結(jié)是很有必要的。

緩存穿透 強(qiáng)調(diào)是獲取本不存在的緩存數(shù)據(jù),請(qǐng)求必然會(huì)越過(guò)緩存層直接到達(dá)到存儲(chǔ)層,很明顯這是利用業(yè)務(wù)規(guī)則的漏洞對(duì)系統(tǒng)發(fā)起攻擊,解決方案的核心原則是過(guò)濾這些非法業(yè)務(wù)請(qǐng)求,與是否是熱點(diǎn)數(shù)據(jù)、緩存失效時(shí)間等因素沒(méi)有關(guān)系。

緩存擊穿 強(qiáng)調(diào)的是熱點(diǎn)key的失效,導(dǎo)致某一時(shí)刻大量請(qǐng)求會(huì)直接到db層,解決方案的核心原則是規(guī)避數(shù)據(jù)庫(kù)的并發(fā)操作。

緩存雪崩 強(qiáng)調(diào)的多個(gè)key的集體失效,與key是否是熱點(diǎn)數(shù)據(jù)并不是必然的因素,解決方案的核心原則則讓key之間的失效時(shí)間分布更加均勻,避免集體失效的情況。


3? 數(shù)據(jù)更新場(chǎng)景分析

引入緩存后數(shù)據(jù)會(huì)分別存放到緩存以及數(shù)據(jù)庫(kù)兩個(gè)地方,因此數(shù)據(jù)更新時(shí),需要涉及到這兩處地方得更新,并且更新時(shí)序的不同會(huì)有不同的結(jié)果。關(guān)于數(shù)據(jù)更新目前業(yè)界已經(jīng)沉淀了Cache Aside Pattern,Read/Write through等多種方式。

3.1 Cache Aside Pattern(緩存靠邊站)

這是很通用的更新策略,主要流程如下:


讀取


更新數(shù)據(jù)

主要涉及一下幾點(diǎn):

失效:應(yīng)用程序從cache中讀取數(shù)據(jù),沒(méi)有查詢(xún)到,從db中查詢(xún)到數(shù)據(jù),然后更新緩;

命中:應(yīng)用程序從cache中查詢(xún)到數(shù)據(jù),直接返回;

更新:應(yīng)用程序?qū)?shù)據(jù)更新到數(shù)據(jù)庫(kù),成功后,更新緩存。

Cache Aside Pattern 采用的是先更新數(shù)據(jù)庫(kù),再失效緩存的(刪除key),為什么采取這種方式?

1)為什么不是更新緩存,而是失效緩存(刪除key)

? 并發(fā)寫(xiě)容易造成寫(xiě)覆蓋,造成臟數(shù)據(jù):

當(dāng)數(shù)據(jù)發(fā)生更新時(shí),一般有兩種方式處理緩存數(shù)據(jù),一種是更新緩存,一種是失效數(shù)據(jù),由下一次讀請(qǐng)求更新緩存數(shù)據(jù),假設(shè)是更新緩存的話,在并發(fā)情況下存在多線程寫(xiě)緩存造成臟數(shù)據(jù)的情況,如下圖:


并發(fā)更新數(shù)據(jù)

如上圖所示,假設(shè)A、B兩個(gè)線程,A先更新數(shù)據(jù)庫(kù)后 B再更新數(shù)據(jù)庫(kù),然后分別進(jìn)行更新緩存,但是B先更新緩存成功,A后更新緩存成功,這樣就導(dǎo)致數(shù)據(jù)庫(kù)是最新的數(shù)據(jù)但是緩存中是舊的臟數(shù)據(jù)。而如果失效緩存數(shù)據(jù)的話,可以保證下一次讀請(qǐng)求回源到數(shù)據(jù)庫(kù)將最新的數(shù)據(jù)載入到緩存中,避免臟數(shù)據(jù)的問(wèn)題。因此,針對(duì)數(shù)據(jù)更新緩存采用失效的方式進(jìn)行處理,也可以參考這篇文章《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》。包括Facebook的論文《Scaling Memcache at Facebook》也使用了這個(gè)策略。

? 雙寫(xiě)不同數(shù)據(jù)源容易造成數(shù)據(jù)不一致:同時(shí)寫(xiě)數(shù)據(jù)庫(kù)以及更新緩存,任何一個(gè)更新失敗都會(huì)造成臟數(shù)據(jù)。如果事務(wù)都成功了,在并發(fā)情況下,雙寫(xiě)也很難保證雙寫(xiě)都成功,看一看一下下面的時(shí)序圖:


先更新緩存,再更新數(shù)據(jù)庫(kù)


先更新數(shù)據(jù)庫(kù),再更新緩存

? 違背數(shù)據(jù)懶加載,避免不必要的計(jì)算消耗:如果有些緩存值是需要經(jīng)過(guò)復(fù)雜的計(jì)算才能得出,所以如果每次更新數(shù)據(jù)的時(shí)候都更新緩存,但是后續(xù)在一段時(shí)間內(nèi)并沒(méi)有讀取該緩存數(shù)據(jù),這樣就白白浪費(fèi)了大量的計(jì)算性能,完全可以后續(xù)由讀請(qǐng)求的時(shí)候,再去計(jì)算即可,這樣更符合數(shù)據(jù)懶加載,降低計(jì)算開(kāi)銷(xiāo)。

2)可能存在的更新時(shí)序

在確定數(shù)據(jù)更新后緩存會(huì)失效來(lái)進(jìn)行處理的話,針對(duì)數(shù)據(jù)庫(kù)以及緩存更新時(shí)序就存在如下這幾種:

? 先失效緩存再更新數(shù)據(jù)庫(kù)


先失效緩存

這種情況的問(wèn)題有哪些?

如時(shí)序圖所示,線程A先失效緩存數(shù)據(jù)的時(shí)候,B線程讀請(qǐng)求發(fā)現(xiàn)緩存數(shù)據(jù)為空的話,就會(huì)從數(shù)據(jù)庫(kù)中讀取舊值放入到緩存中,這樣就導(dǎo)致后續(xù)的讀請(qǐng)求讀到的都是緩存中的臟數(shù)據(jù)。針對(duì)這樣的情況可以采用延時(shí)雙刪的策略來(lái)有效避免,偽代碼 如下:

cache.delKey(key);

db.update(data);

Thread.sleep(xxx);

cache.delKey(key);

主要是在寫(xiě)請(qǐng)求更新完數(shù)據(jù)庫(kù)后進(jìn)行休眠一段時(shí)間,然后刪除可能由讀請(qǐng)求帶來(lái)的臟數(shù)據(jù)存入到緩存。另外,數(shù)據(jù)庫(kù)如果采用的是主從分離的架構(gòu)的話,讀出來(lái)的數(shù)據(jù)也有可能是主從未同步完成造成的臟數(shù)據(jù)。這種通過(guò)延時(shí)雙刪的方式需要線程休眠,因此很顯然會(huì)降低系統(tǒng)吞吐量,并不是一種優(yōu)雅的解決方式,也可以采用異步刪除的方式。當(dāng)然可以設(shè)置過(guò)期時(shí)間,到期后緩存失效載入最新的數(shù)據(jù),需要系統(tǒng)能夠容忍一段時(shí)間的數(shù)據(jù)不一致。

? 先更新數(shù)據(jù)庫(kù)再失效緩存 (推薦時(shí)序)

先更新數(shù)據(jù)庫(kù)


這種方式也會(huì)存在數(shù)據(jù)不一致的情況

假設(shè)緩存剛好到期失效時(shí),讀請(qǐng)求從db中讀取數(shù)據(jù),寫(xiě)請(qǐng)求更新完數(shù)據(jù)后再失效緩存后,讀請(qǐng)求將舊數(shù)據(jù)存入到緩存中,這種情況也會(huì)導(dǎo)致臟數(shù)據(jù)的問(wèn)題。實(shí)際上這種情況發(fā)生的概率很低,針對(duì)這種”邏輯失敗“造成的數(shù)據(jù)不一致,可以采用上面所說(shuō)的異步雙刪的策略以及過(guò)期失效的方式來(lái)避免。

這個(gè)case理論上會(huì)出現(xiàn),不過(guò),實(shí)際上出現(xiàn)的概率可能非常低,因?yàn)檫@個(gè)條件需要發(fā)生在讀緩存時(shí)緩存失效,而且并發(fā)著有一個(gè)寫(xiě)操作。而實(shí)際上數(shù)據(jù)庫(kù)的寫(xiě)操作會(huì)比讀操作慢得多,而且還要鎖表,而讀操作必需在寫(xiě)操作前進(jìn)入數(shù)據(jù)庫(kù)操作,而又要晚于寫(xiě)操作更新緩存,所有的這些條件都具備的概率基本并不大。

3.2 Write/Read Through

Cache Aside Pattern 對(duì)db 以及緩存的更新邏輯是由調(diào)用方去控制的,這種邏輯比較復(fù)雜。

Write/Read Through對(duì)調(diào)用方而言,緩存是作為整個(gè)的數(shù)據(jù)存儲(chǔ),而不用關(guān)系緩存后面的db,數(shù)據(jù)庫(kù)的更新則是由緩存統(tǒng)一進(jìn)行管理,對(duì)調(diào)用方而言只需要和緩存進(jìn)行交互,整體過(guò)程是透明的。

1)Read Through:當(dāng)數(shù)據(jù)發(fā)生更新時(shí),查詢(xún)緩存時(shí)更新緩存,然后由緩存層同步的更新數(shù)據(jù)庫(kù)即可,對(duì)調(diào)用方而言只需要和緩存層交互即可;

2)Write Through:Write Through 套路和Read Through相仿,不過(guò)是在更新數(shù)據(jù)時(shí)發(fā)生。當(dāng)有數(shù)據(jù)更新的時(shí)候,如果沒(méi)有命中緩存,直接更新數(shù)據(jù)庫(kù),然后返回。如果命中了緩存,則更新緩存,然后再由Cache自己同步更新數(shù)據(jù)庫(kù)。如下圖所示:

read/write through

3.3 write behind cache pattern

這種如果數(shù)據(jù)更新,會(huì)直接更新緩存,然后起一個(gè)異步任務(wù)去更新數(shù)據(jù)庫(kù)。這種異步方式請(qǐng)求響應(yīng)會(huì)很快,系統(tǒng)的吞吐量會(huì)明顯提升。但是,因?yàn)槭钱惒礁聰?shù)據(jù)庫(kù),數(shù)據(jù)一致性的保障就會(huì)變?nèi)酰绻聰?shù)據(jù)庫(kù)失敗則會(huì)永遠(yuǎn)的造成系統(tǒng)臟數(shù)據(jù),需要很精細(xì)設(shè)計(jì)系統(tǒng)重試的策略,另外如果異步服務(wù)宕機(jī)的話,還要考慮更新的數(shù)據(jù)如何持久化,服務(wù)重啟后能夠迅速恢復(fù)。在更新數(shù)據(jù)庫(kù)時(shí),由于并發(fā)多任務(wù)的存在,還需要考慮并發(fā)寫(xiě)是否會(huì)造成臟數(shù)據(jù)的問(wèn)題,就需要追溯每次更新數(shù)據(jù)的時(shí)序。使用這種模式需要考慮的細(xì)節(jié)會(huì)有很多,設(shè)計(jì)出一套好的方案是件很不容易的事情。

總結(jié):這四種方式的更新策略都是很常用的,也是業(yè)界經(jīng)過(guò)大規(guī)模業(yè)務(wù)總結(jié)出來(lái)的,而著幾種方案主要的關(guān)注點(diǎn):

1、最新的數(shù)據(jù)應(yīng)該放置在哪里?

使用緩存是為了提高系統(tǒng)的性能,利用內(nèi)存的IO讀取的高速特性,來(lái)提升系統(tǒng)的性能,提高吞吐量;緩存的存在可以減少一部分請(qǐng)求打到db,減小db的壓力,畢竟db最容易出現(xiàn)瓶頸。但是帶來(lái)的問(wèn)題就是,數(shù)據(jù)存在兩個(gè)地方,當(dāng)數(shù)據(jù)更新的時(shí)候需要思考如何讓 正確的數(shù)據(jù)放到最可信的存儲(chǔ)介質(zhì)上,這個(gè)就需要結(jié)合業(yè)務(wù)的性質(zhì),在兩種介質(zhì)中做選擇。

Cache Aside Pattern選擇先更新數(shù)據(jù)庫(kù),再失效緩存,這樣可以保證最新最正確的數(shù)據(jù)一定會(huì)落在數(shù)據(jù)庫(kù)中,這樣可以保證核心的業(yè)務(wù)數(shù)據(jù)在數(shù)據(jù)庫(kù)中一定是可信的,但是帶來(lái)的問(wèn)題是業(yè)務(wù)邏輯更復(fù)雜,系統(tǒng)處理更新邏輯耗時(shí)更長(zhǎng)。

如果是非核心數(shù)據(jù)的更新,可以選擇write behind cache pattern的方式,只需要更新緩存即可,能夠快速的響應(yīng)。缺點(diǎn)是很容易造成數(shù)據(jù)不一致,數(shù)據(jù)庫(kù)中的數(shù)據(jù)不一定的就是最可信的數(shù)據(jù)。

所以,不同的更新策略實(shí)際上也是將最新的數(shù)據(jù)優(yōu)先選擇放在哪里更合適以及系統(tǒng)性能的一種權(quán)衡,需要結(jié)合業(yè)務(wù)場(chǎng)景做好trade-off。

4、數(shù)據(jù)不一致

4.1 數(shù)據(jù)不一致的原因

由于引入了緩存,數(shù)據(jù)會(huì)分散在兩處不同的數(shù)據(jù)源當(dāng)數(shù)據(jù)更新的時(shí)候,是很難做到數(shù)據(jù)一致的,除非采用強(qiáng)制一致性方案。我們需要根據(jù)數(shù)據(jù)不一致的原因,找出合理的解決方案

1)邏輯失敗造成的數(shù)據(jù)不一致

之前提到的四種數(shù)據(jù)更新策略,在并發(fā)情況下,無(wú)論了先山緩存還是更新數(shù)據(jù)庫(kù),還是更新數(shù)據(jù)庫(kù)再刪緩存,都會(huì)出現(xiàn)數(shù)據(jù)不一致的情況,主要是因?yàn)楫惒阶x寫(xiě)請(qǐng)求再并發(fā)情況下的操作時(shí)序的數(shù)據(jù)不一致,這種不一致情況叫做 邏輯失敗。

這種因?yàn)椴l(fā)時(shí)序問(wèn)題造成的問(wèn)題,核心的解決思想是將異步的操作進(jìn)行串行化。

2)物理失敗造成的數(shù)據(jù)不一致

Cache Aside Pattern 中先更新數(shù)據(jù)庫(kù),再刪除緩存以及異步雙刪策略等,如果刪除緩存失敗時(shí)就會(huì)出現(xiàn)不一致的情況。但是數(shù)據(jù)庫(kù)更新和緩存更新沒(méi)辦法放到一個(gè)事務(wù)中,一般來(lái)說(shuō)使用的緩存都是分布式緩存,如果緩存服務(wù)很耗時(shí),那么將更新數(shù)據(jù)庫(kù)和更新緩存放到一個(gè)事務(wù)中,會(huì)造成大量的數(shù)據(jù)庫(kù)鏈接刮起,嚴(yán)重的影響系統(tǒng)性能,甚至?xí)驗(yàn)閿?shù)據(jù)庫(kù)鏈接數(shù)過(guò)多,導(dǎo)致系統(tǒng)崩潰。像這種因?yàn)榫彺娌僮魇。瑢?dǎo)致的數(shù)據(jù)不一致情況稱(chēng)之為物理失敗。

大多數(shù)情況物理失敗采用重試的方式進(jìn)行解決。

4.2 數(shù)據(jù)一致性的解決方案

在絕大部分業(yè)務(wù)場(chǎng)景中,追求的是最終一致性,針對(duì)物理失敗造成的數(shù)據(jù)不一致常用的方案有:消費(fèi)消息異步刪除緩存以及訂閱Binlog的方式,針對(duì)邏輯失敗造成的數(shù)據(jù)不一致常用的方案有:隊(duì)列異步操作同步化。

4.2.1消費(fèi)消息異步刪除緩存


消費(fèi)消息異步刪除緩存

4.2.2 訂閱Binlog

訂閱binlog

4.2.3 利用隊(duì)列串行化

在分析cache aside pattern發(fā)現(xiàn)在并發(fā)的情況下也會(huì)存在數(shù)據(jù)不一致的場(chǎng)景,只不過(guò)發(fā)生的概率很低,另外如果先刪除緩存再更新數(shù)據(jù)庫(kù)在并發(fā)讀寫(xiě)的情況下也會(huì)存在數(shù)據(jù)不一致的情況。類(lèi)似這種由于并發(fā)時(shí)序?qū)е碌臄?shù)據(jù)不一致的情況,都是因?yàn)閷?xiě)請(qǐng)求還沒(méi)有結(jié)束讀請(qǐng)求讀取的是舊數(shù)據(jù),如果讀請(qǐng)求在寫(xiě)請(qǐng)求之后處理,即請(qǐng)求的處理能夠串行化的話,就能保證讀請(qǐng)求讀到的是寫(xiě)請(qǐng)求更新的最新的數(shù)據(jù)。

將請(qǐng)求進(jìn)行串行化,最常用的方式是采用隊(duì)列的方式,一個(gè)隊(duì)列只能對(duì)應(yīng)一個(gè)工作線程,更新數(shù)據(jù)的寫(xiě)請(qǐng)求放置隊(duì)列中,等待異步處理;讀請(qǐng)求如果能從緩存中獲取數(shù)據(jù),則返回,如果緩存中沒(méi)有數(shù)據(jù),就將讀請(qǐng)求放置到隊(duì)列中,等待寫(xiě)請(qǐng)求數(shù)據(jù)更新完成。這種方案需要考慮的問(wèn)題有:

1)讀請(qǐng)求長(zhǎng)時(shí)間阻塞:如果隊(duì)列中擠壓了多個(gè)寫(xiě)請(qǐng)求,則讀請(qǐng)求會(huì)存在長(zhǎng)時(shí)間阻塞的情況,需要設(shè)置超時(shí)處理策略,一旦超過(guò)超時(shí)時(shí)間,則直接讀取數(shù)據(jù)庫(kù)返回,避免長(zhǎng)時(shí)間不響應(yīng);另外,在業(yè)務(wù)中需要進(jìn)行壓測(cè),考慮隊(duì)列中在峰值情況下會(huì)積攢多少寫(xiě)請(qǐng)求,如果過(guò)多,需要考慮隊(duì)列優(yōu)化的方式和相應(yīng)的解決方案;

2)多個(gè)隊(duì)列分散壓力:可以根據(jù)數(shù)據(jù)項(xiàng)通過(guò)hash等路由方式,創(chuàng)建多個(gè)隊(duì)列并行執(zhí)行來(lái)提升系統(tǒng)吞吐量;

3)操作復(fù)雜需要考慮全面:由于采用隊(duì)列來(lái)進(jìn)行串行化,那么要考慮隊(duì)列的可用性,隊(duì)列阻塞以及服務(wù)掛掉后的容災(zāi)恢復(fù)策略是否健壯等等,相對(duì)而言整體的方案需要考慮的點(diǎn)會(huì)有很多;

這種方式可以做到數(shù)據(jù)強(qiáng)一致性,由于串行化系統(tǒng)的吞吐量會(huì)下降很多并且操作復(fù)雜,畢竟任何方案都會(huì)有利弊權(quán)衡的過(guò)程,需要根據(jù)業(yè)務(wù)場(chǎng)景選擇合適的技術(shù)方案。針對(duì)數(shù)據(jù)強(qiáng)一致性很有很多方案,但基本上操作設(shè)計(jì)都很復(fù)雜,在大多數(shù)業(yè)務(wù)場(chǎng)景滿(mǎn)足數(shù)據(jù)最終一致性即可。

當(dāng)然除了以上這三種通用的方法外,為緩存設(shè)置過(guò)期時(shí)間以及定時(shí)全量同步,也是接近最終一致性的最簡(jiǎn)單以及有效的方式。

5、常見(jiàn)的幾個(gè)場(chǎng)景問(wèn)題

在分析數(shù)據(jù)更新的策略后發(fā)現(xiàn)正確使用緩存是一件很不容易的事情,在實(shí)際使用緩存時(shí),還會(huì)有很多有意思的場(chǎng)景(”坑“),在這里進(jìn)行一下總結(jié):

1)過(guò)期還是不過(guò)期緩存數(shù)據(jù):針對(duì)緩存數(shù)據(jù)是否需要設(shè)置過(guò)期時(shí)間也需要結(jié)合場(chǎng)景來(lái)進(jìn)行分析,一些產(chǎn)品信息,大多數(shù)數(shù)據(jù)在業(yè)務(wù)中都是讀場(chǎng)景更多,并且緩存空間很大的話,就可以考慮不過(guò)期數(shù)據(jù)。那是否就意味著這就是一份靜態(tài)數(shù)據(jù)了?當(dāng)緩存空間已滿(mǎn)時(shí),數(shù)據(jù)會(huì)根據(jù)淘汰策略移除緩存,另外數(shù)據(jù)更新時(shí)也可以通過(guò)Binlog等其他方式進(jìn)行異步失效緩存。

如果系統(tǒng)通過(guò)消息異步更新操作成本過(guò)高或者依賴(lài)于外部系統(tǒng)無(wú)法進(jìn)行訂閱binlog異步更新的話,就需要來(lái)采用過(guò)期緩存數(shù)據(jù)來(lái)保障數(shù)據(jù)最終一致性。

2)維度化緩存與增量更新:如果一個(gè)實(shí)體包含多個(gè)屬性,在實(shí)體發(fā)生變更時(shí),如果將所有的屬性全部更新一遍,這個(gè)成本就很高,況且只是其中的幾個(gè)屬性發(fā)生變化。因此,將多個(gè)屬性進(jìn)行各個(gè)維度化進(jìn)行拆解,按照多維度進(jìn)行緩存,更新時(shí)只需要增強(qiáng)更新對(duì)應(yīng)維度即可;

3)大value:大value的問(wèn)題要時(shí)刻警惕,可以考慮將value進(jìn)行壓縮,以及緩存時(shí)進(jìn)行拆解,然后在業(yè)務(wù)服務(wù)中進(jìn)行數(shù)據(jù)聚合來(lái)避免大value的問(wèn)題;

4)熱點(diǎn)緩存問(wèn)題:針對(duì)熱點(diǎn)數(shù)據(jù)如果每次都從遠(yuǎn)程緩存去獲取,會(huì)給緩存系統(tǒng)帶來(lái)過(guò)多的負(fù)載,會(huì)導(dǎo)致獲取緩存數(shù)據(jù)響應(yīng)過(guò)慢,可以使用緩存集群,掛載更多的從緩存,讀取數(shù)據(jù)從從緩存中獲取。針對(duì)熱點(diǎn)數(shù)據(jù)可以使用應(yīng)用本地緩存來(lái)減少對(duì)遠(yuǎn)程緩存的請(qǐng)求負(fù)載;

5)數(shù)據(jù)預(yù)熱:可以預(yù)先將數(shù)據(jù)加載到緩存中,方式緩存數(shù)據(jù)為空,大量的請(qǐng)求回源到db。如果容量很高可以考慮全量預(yù)熱,如果容量?jī)?yōu)先,就只能選擇高頻熱點(diǎn)數(shù)據(jù)進(jìn)行數(shù)據(jù)預(yù)熱,還需要關(guān)注是否有批量操作以及慢sql帶來(lái)的性能問(wèn)題,在整個(gè)數(shù)據(jù)預(yù)熱過(guò)程中需要有可靠的監(jiān)控機(jī)制來(lái)保障;

6)非預(yù)期熱點(diǎn)數(shù)據(jù):針對(duì)業(yè)務(wù)預(yù)估不足的熱點(diǎn)數(shù)據(jù),需要有熱點(diǎn)發(fā)現(xiàn)系統(tǒng)來(lái)統(tǒng)計(jì)熱點(diǎn)key,實(shí)時(shí)監(jiān)控非預(yù)期的熱點(diǎn)數(shù)據(jù),可以將這些key推到本地緩存中,防止預(yù)估不足的熱點(diǎn)key拖垮遠(yuǎn)程緩存服務(wù)。

7)緩存實(shí)例故障快速恢復(fù):當(dāng)某一個(gè)緩存實(shí)例故障后,緩存一般是采用分片實(shí)例存儲(chǔ),假設(shè)緩存key路由策略采用的取模機(jī)制的話,會(huì)導(dǎo)致當(dāng)前實(shí)例的流量迅速到達(dá)db層,這種情況可以采用主從機(jī)制,當(dāng)一個(gè)實(shí)例故障后其他實(shí)例可以使用,但是這種方式的問(wèn)題在于水平擴(kuò)展不夠,如果分片實(shí)例上增加一個(gè)節(jié)點(diǎn)的話,會(huì)導(dǎo)致緩存命中率迅速下降。

如果key路由策略采用的一致性哈希的話,某一個(gè)實(shí)例節(jié)點(diǎn)故障,只會(huì)導(dǎo)致哈希環(huán)上的部分緩存不命中不會(huì)導(dǎo)致大量請(qǐng)求到達(dá)db,但是針對(duì)熱點(diǎn)數(shù)據(jù)的話,可能會(huì)導(dǎo)致該節(jié)點(diǎn)負(fù)載過(guò)高成為系統(tǒng)瓶頸。

針對(duì)實(shí)例故障恢復(fù)的方式有:

1. 主從機(jī)制,對(duì)數(shù)據(jù)進(jìn)行備份,盡可能保障有可用數(shù)據(jù);

2. 服務(wù)降低,新增緩存實(shí)例然后異步線程預(yù)熱數(shù)據(jù);

3. 可以先采用一致性哈希路由策略,當(dāng)出現(xiàn)熱點(diǎn)數(shù)據(jù)時(shí)到達(dá)某個(gè)閾值時(shí)降級(jí)為取模的策略。

6. 幾個(gè)影響因素

影響緩存整體的性能會(huì)有很多大大小小的影響因素,比如語(yǔ)言本身的特性的影響,例如Java需要考慮GC的影響。還需要盡可能的提升緩存命中率等等多個(gè)方面,總結(jié)下來(lái),核心的幾個(gè)影響因素如下:

1)提升緩存命中率:影響緩存命中率的幾個(gè)因素:

? 業(yè)務(wù)時(shí)效性要求:緩存適合"讀多寫(xiě)少"的業(yè)務(wù)場(chǎng)景,并且業(yè)務(wù)性質(zhì)決定了時(shí)效性要求,不同的時(shí)效性要求決定了緩存的更新策略以及過(guò)期時(shí)間,對(duì)時(shí)效性也低的業(yè)務(wù)越適合使用緩存,并且緩存命中率越高;

? 緩存粒度設(shè)計(jì):通常而言,緩存對(duì)象粒度越小就越適合使用緩存,不會(huì)導(dǎo)致頻繁更新導(dǎo)致緩存命中率下降;

? 緩存淘汰策略:如果緩存空間有限,不同的緩存淘汰策略也會(huì)影響緩存命中率,如果淘汰的緩存數(shù)據(jù)后續(xù)被大量使用,無(wú)疑就會(huì)降低緩存命中率;

? 緩存部署方式:在使用分布式緩存時(shí),要做好容量規(guī)劃以及容災(zāi)策略,以免緩存實(shí)例故障后造成大規(guī)模緩存失效;

? Key路由策略:不同路由策略會(huì)在節(jié)點(diǎn)實(shí)例故障后帶來(lái)不同的影響,如果采用取模的方式水平擴(kuò)展時(shí)則會(huì)降低緩存命中率。通過(guò)這些分析,提高緩存命中率沒(méi)有放之四海而皆準(zhǔn)的統(tǒng)一規(guī)則,需要從這些角度去思考,盡可能的在高頻訪問(wèn)且時(shí)效性不是很高的業(yè)務(wù)數(shù)據(jù)上使用緩存。

2)序列化方式:使用遠(yuǎn)程緩存服務(wù)免不了需要經(jīng)過(guò)序列化后在網(wǎng)絡(luò)中進(jìn)行數(shù)據(jù)傳輸,那么選擇不同的序列化方式對(duì)緩存性能會(huì)有影響。選擇序列化方式時(shí)需要考慮序列化耗時(shí)、序列化后在網(wǎng)絡(luò)傳輸中包大小以及序列化的計(jì)算開(kāi)銷(xiāo)。

3)GC影響:采用多級(jí)緩存以及大value時(shí)會(huì)采用應(yīng)用本地緩存,對(duì)于java應(yīng)用,就需要考慮大對(duì)象帶來(lái)的GC影響。

4)緩存協(xié)議:了解不同的緩存協(xié)議的優(yōu)缺點(diǎn)比如Redis以及Memcached協(xié)議,根據(jù)業(yè)務(wù)場(chǎng)景進(jìn)行選擇。

5)緩存連接池:為提升訪問(wèn)性能,需要合理的設(shè)置緩存連接池。

6)完善的監(jiān)控平臺(tái):需要考慮是否有一套緩存的監(jiān)控平臺(tái),能夠追蹤緩存使用情況、緩存服務(wù)整體的性能以及一些非預(yù)期熱點(diǎn)數(shù)據(jù)的發(fā)現(xiàn)策略等等,這樣才能綜合整體的保障緩存服務(wù)的可用以及性能。

7. 多級(jí)緩存設(shè)計(jì)案例

從用戶(hù)發(fā)出請(qǐng)求到到最底層的數(shù)據(jù)庫(kù)實(shí)際上會(huì)經(jīng)歷很多節(jié)點(diǎn),因此在整個(gè)鏈路上都可以設(shè)置緩存,并且按照緩存最近原則將緩存放置在里用戶(hù)最近的地方提升系統(tǒng)響應(yīng)的效果最為明顯,相應(yīng)的提升系統(tǒng)吞吐量的效果就越為顯著,通過(guò)能夠大大降低對(duì)后端的壓力。在整個(gè)鏈路流程里可以添加緩存的地方有:發(fā)起請(qǐng)求-->瀏覽器/客戶(hù)端緩存-->邊緣緩存/CDN-->反向代理(Nginx)緩存-->遠(yuǎn)程緩存-->進(jìn)程內(nèi)緩存-->數(shù)據(jù)庫(kù)緩存。服務(wù)端多級(jí)緩存設(shè)計(jì)通用的技術(shù)方案如下:


多級(jí)緩存

主要流程為:

1)請(qǐng)求先達(dá)到Nginx,先讀取Nginx本地緩存,如果命中緩存則返回緩存數(shù)據(jù)。這里的負(fù)載均衡路由策略,采用輪詢(xún)的方式相對(duì)而言訪問(wèn)壓力分布的更加均衡,一致性哈希方式能夠提升緩存命中率,但是同時(shí)也會(huì)存在單點(diǎn)壓力過(guò)大的問(wèn)題,可以考慮使用一致性哈希策略時(shí)流量達(dá)到一定閾值的時(shí)候切換成輪詢(xún)的方式;

2)如果沒(méi)有命中Nginx緩存,則讀取分布式緩存,為了高可用以及提升系統(tǒng)吞吐量,一般遠(yuǎn)程分布式緩存會(huì)采用主從結(jié)構(gòu),這里讀取的就是從緩存服務(wù)集群數(shù)據(jù),如果命中緩存則返回?cái)?shù)據(jù);

3)如果從緩存沒(méi)有命中緩存,則讀取應(yīng)用本地緩存(堆內(nèi)/堆外緩存),這里的路由策略同樣可以采用輪詢(xún)或者一致性哈希。如果命中,則返回?cái)?shù)據(jù),并回寫(xiě)到Nginx緩存中;為避免由于從緩存服務(wù)出現(xiàn)問(wèn)題,造成過(guò)大的流量沖垮數(shù)據(jù)庫(kù),這里可以嘗試讀取主緩存服務(wù);

4)如果所有緩存沒(méi)有命中,則查詢(xún)數(shù)據(jù)庫(kù)并返回?cái)?shù)據(jù),并異步回寫(xiě)到主緩存以及應(yīng)用本地緩存中。主緩存通過(guò)主從同步機(jī)制同步到從緩存服務(wù)集群中。這里會(huì)寫(xiě)到主緩存的時(shí)候需要考慮多個(gè)應(yīng)用實(shí)例在異步寫(xiě),需要考慮數(shù)據(jù)是否會(huì)亂序的問(wèn)題。

另外,對(duì)于一些非預(yù)期熱點(diǎn)數(shù)據(jù)比如微博中”某某明星結(jié)婚“等等熱門(mén)話題帶來(lái)的訪問(wèn)流量瞬間沖擊到后端,針對(duì)以上多級(jí)緩存設(shè)計(jì),可以通過(guò)引入熱點(diǎn)發(fā)現(xiàn)系統(tǒng)來(lái)發(fā)現(xiàn)非預(yù)期的熱點(diǎn)數(shù)據(jù),利用flume訂閱Nginx日志,然后通過(guò)消息進(jìn)行消費(fèi),最后通過(guò)storm等實(shí)時(shí)計(jì)算框架進(jìn)行熱點(diǎn)數(shù)據(jù)的統(tǒng)計(jì),當(dāng)監(jiān)控發(fā)現(xiàn)到熱點(diǎn)數(shù)據(jù),將其推送到各個(gè)緩存節(jié)點(diǎn)上,整體的緩存設(shè)計(jì)如下:


非預(yù)期熱點(diǎn)數(shù)據(jù)監(jiān)控

8. 總結(jié)

為了追求高性能,每個(gè)開(kāi)發(fā)者最先使用的就是緩存,也在潛意識(shí)里將緩存作為了系統(tǒng)性能瓶頸的一劑良藥,經(jīng)過(guò)系統(tǒng)化的總結(jié)和分析緩存后,就可以發(fā)現(xiàn)緩存如果使用不當(dāng)真的就會(huì)事與愿違,成為毒藥,并不會(huì)系統(tǒng)迭代出那個(gè)局部最優(yōu)解。如果貿(mào)然的使用緩存,需要考慮的地方真的很多稍有不注意,反而會(huì)讓系統(tǒng)投入更多的維護(hù)成本,陡增更高的復(fù)雜度。那是不是就不使用緩存呢?也不是,緩存在高并發(fā)的情況下通過(guò)IO高速的緩存獲取數(shù)據(jù)能使得每個(gè)請(qǐng)求能夠快速響應(yīng),并且能夠大大提升系統(tǒng)吞吐量以及支撐更高的并發(fā)用戶(hù)數(shù),在現(xiàn)有的高并發(fā)大流量的互聯(lián)網(wǎng)應(yīng)用中應(yīng)用緩存的例子太多了,也足以證明緩存在優(yōu)化系統(tǒng)整體性能是一種行之有效的方案。

作為開(kāi)發(fā)者不是每個(gè)人都有機(jī)會(huì)和機(jī)遇去挑戰(zhàn)高并發(fā)的互聯(lián)網(wǎng)架構(gòu)以及高量級(jí)的訪問(wèn)流量和應(yīng)用規(guī)模的,那是不是就意味著這些通用的技術(shù)方案就不用深刻分析呢?很顯然不是,單從緩存使用中就會(huì)發(fā)現(xiàn)在高并發(fā)下讀寫(xiě)帶來(lái)的數(shù)據(jù)不一致性分析下來(lái)就會(huì)有很多并發(fā)場(chǎng)景,單線程下都是正常的,但在并發(fā)下就會(huì)出現(xiàn)很多意想不到的case,而這些分析的思路是最核心的,也是開(kāi)發(fā)者逐漸形成自己的方法論的有效訓(xùn)練途徑。在系統(tǒng)化學(xué)習(xí)每一種技術(shù)組件時(shí),業(yè)界的通用解決方案都是經(jīng)過(guò)歷史經(jīng)驗(yàn)慢慢沉淀下來(lái)的智慧,如同品酒,是需要靜下心來(lái)好好去品的。

技術(shù)最終是服務(wù)于業(yè)務(wù)價(jià)值,而業(yè)務(wù)規(guī)模擴(kuò)張會(huì)反哺技術(shù)的創(chuàng)新,要設(shè)計(jì)出一套適應(yīng)于業(yè)務(wù)的合理的技術(shù)方案,需要很深的內(nèi)功,需要既懂技術(shù)又要對(duì)業(yè)務(wù)理解十分深刻才行,懂業(yè)務(wù)而不懂技術(shù),很難知道每種技術(shù)方案的局限性,也就是經(jīng)常所說(shuō)的PPT架構(gòu)師,PPT很炫酷,一頓操作猛如虎但是并不是最適合業(yè)務(wù)的那個(gè)解,反而就像是跳梁小丑一樣自嗨或者帶著功利心去急于變現(xiàn),只有業(yè)務(wù)與技術(shù)結(jié)合能夠得到最大價(jià)值的那個(gè)解就是最合適的方案,需要在優(yōu)與劣的trade-off上做出權(quán)衡。如果很懂技術(shù),但是不懂業(yè)務(wù),同樣的就是廢銅爛鐵沒(méi)辦法發(fā)揮出功力。在不同的職業(yè)生涯階段,每個(gè)人的精力有限,投入技術(shù)以及業(yè)務(wù)的精力分配也是不同的,專(zhuān)注的點(diǎn)會(huì)有所不同,就像業(yè)務(wù)與技術(shù)一樣,在人生的賽道中在不同階段也需要迭代出那個(gè)最合適的局部最優(yōu)解,至于什么最合適,答案在每個(gè)人心中!

大家可以參考《深入分布式緩存》 這本資料。

最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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