響應(yīng)速度不給力?解鎖正確緩存姿勢(shì)
1. 常見概念
在合理應(yīng)用緩存前,需要了解緩存領(lǐng)域里相關(guān)的幾個(gè)常用術(shù)語:
1)緩存命中:表示數(shù)據(jù)能夠從緩存中獲取,不需要回源;
2)Cache miss:表示沒有命中緩存,如果緩存內(nèi)存中還有內(nèi)存空間的話,會(huì)將數(shù)據(jù)加入到緩存中;
3)存儲(chǔ)成本:當(dāng)沒有命中緩存時(shí),回源獲取后會(huì)將數(shù)據(jù)放置到存儲(chǔ)中,整個(gè)將數(shù)據(jù)放置到存儲(chǔ)空間所需要的時(shí)間以及空間稱之為存儲(chǔ)成本;
4)緩存失效:當(dāng)源數(shù)據(jù)發(fā)生變更后,意味著緩存中的數(shù)據(jù)失效;
5)緩存污染:將不經(jīng)常訪問的數(shù)據(jù)放置到緩存存儲(chǔ)空間中,以至于高頻訪問的數(shù)據(jù)無法放置到緩存中;
6)替代策略:當(dāng)數(shù)據(jù)放置到緩存空間時(shí),由于空間不足時(shí),就需要從緩存空間中去除已有的數(shù)據(jù),選擇去除哪些數(shù)據(jù)就是由替代策略決定的。常見的替代策略有如下這些:
- Least-Recently-Used(LRU)
- Least-Frequently-Used(LFU)
- SIZE
- First in First Out(FIFO)
由于存儲(chǔ)空間有限,替代策略要解決的核心問題是盡量保留高頻訪問的緩存數(shù)據(jù),降低緩存污染以提升緩存命中率和整體的緩存效率,難點(diǎn)在于,需要基于數(shù)據(jù)歷史訪問情況,以一種合適的對(duì)未來訪問情況的預(yù)估才能找到更佳的策略。
2. 訪問緩存場(chǎng)景分析
使用緩存通常的操作是,請(qǐng)求先訪問緩存數(shù)據(jù),如果緩存中不存在的話,就會(huì)回源到數(shù)據(jù)庫中然后將數(shù)據(jù)寫入到緩存中;如果存在的話就直接返回?cái)?shù)據(jù)。從整個(gè)過程來看,緩存層就處于數(shù)據(jù)訪問的前置環(huán)節(jié),分擔(dān)數(shù)據(jù)庫在高并發(fā)容易出現(xiàn)系統(tǒng)故障的風(fēng)險(xiǎn),所以在使用過程中需要對(duì)緩存層很謹(jǐn)慎的進(jìn)行分析。在訪問緩存數(shù)據(jù)時(shí),有常見的三大場(chǎng)景:緩存穿透、緩存擊穿以及緩存雪崩。
2.1 緩存穿透
現(xiàn)象:每次請(qǐng)求直接穿透緩存層,直接回源到數(shù)據(jù)庫中,給數(shù)據(jù)庫帶來了巨大訪問壓力,甚至宕機(jī)。
原因:訪問數(shù)據(jù)會(huì)先訪問緩存,如果數(shù)據(jù)不存在緩存中才會(huì)查詢數(shù)據(jù)庫,但是如果查詢數(shù)據(jù)庫也查詢不出來數(shù)據(jù),也是說當(dāng)前訪問數(shù)據(jù)永遠(yuǎn)不會(huì)寫入緩存中。這樣就導(dǎo)致了,訪問一定不存在的數(shù)據(jù),就相當(dāng)于緩存層形同虛設(shè),每次請(qǐng)求都會(huì)到db層,造成數(shù)據(jù)庫負(fù)擔(dān)過大。
解決方案:
- 方案一:采用bloom filter保存緩存過的key,在訪問請(qǐng)求到來時(shí)可以過濾掉不存在的key,防止這些請(qǐng)求到db層;
- 方案二:如果db查詢不到數(shù)據(jù),保存空對(duì)象到緩存層,設(shè)置較短的失效時(shí)間;
- 方案三:針對(duì)業(yè)務(wù)場(chǎng)景對(duì)請(qǐng)求的參數(shù)進(jìn)行有效性校驗(yàn),防止非法請(qǐng)求擊垮db。
2.2 緩存擊穿
現(xiàn)象:當(dāng)某一key失效時(shí),造成大量請(qǐng)求到db層,擊垮存儲(chǔ)層。
原因:為了保證緩存數(shù)據(jù)的時(shí)效性,通常會(huì)設(shè)置一個(gè)失效時(shí)間,如果是熱點(diǎn)key,高并發(fā)時(shí)會(huì)有海量請(qǐng)求直接越過緩存層到數(shù)據(jù)庫,這樣就會(huì)給數(shù)據(jù)庫造成的負(fù)擔(dān)增大,設(shè)置宕機(jī)。
解決方案
方案一:使用互斥鎖,當(dāng)緩存數(shù)據(jù)失效時(shí),保證一個(gè)請(qǐng)求能夠訪問到數(shù)據(jù)庫,并更新緩存,其他線程等待并重試;
方案二:緩存數(shù)據(jù)“永遠(yuǎn)不過期”,如果緩存數(shù)據(jù)不設(shè)置失效時(shí)間的話,就不會(huì)存在熱點(diǎn)key過期造成了大量請(qǐng)求到數(shù)據(jù)庫。但是,緩存數(shù)據(jù)就變成“靜態(tài)數(shù)據(jù)”,因此當(dāng)緩存數(shù)據(jù)快要過期時(shí),采用異步線程的方式提前進(jìn)行更新緩存數(shù)據(jù)。
2.3 緩存雪崩
現(xiàn)象:多個(gè)key失效,造成大量請(qǐng)求到db層,導(dǎo)致db層負(fù)擔(dān)過重甚至宕機(jī)。
原因:緩存雪崩是指在我們?cè)O(shè)置緩存時(shí)采用了相同的過期時(shí)間,導(dǎo)致緩存在某一時(shí)刻同時(shí)失效,請(qǐng)求全部轉(zhuǎn)發(fā)到數(shù)據(jù)庫,最終導(dǎo)致數(shù)據(jù)庫瞬時(shí)壓力過大而崩潰。
解決方案:
方案一:使用互斥鎖的方式,保證只有單個(gè)線程進(jìn)行請(qǐng)求能夠達(dá)到db;
方案二:多每個(gè)key的失效時(shí)間在基礎(chǔ)時(shí)間上再加上一個(gè)1~5分鐘的隨機(jī)值,這樣就能保證大規(guī)模key集體失效的概率,并且需要盡量讓多個(gè)key的失效時(shí)間能夠均勻分布;
2.4 總結(jié)
緩存穿透、緩存擊穿以及緩存雪崩這三個(gè)術(shù)語很容易弄混,也是讀緩存中典型的三個(gè)場(chǎng)景問題,做一下簡單的總結(jié)是很有必要的。緩存穿透強(qiáng)調(diào)是獲取本不存在的緩存數(shù)據(jù),請(qǐng)求必然會(huì)越過緩存層直接到達(dá)到存儲(chǔ)層,很明顯這是利用業(yè)務(wù)規(guī)則的漏洞對(duì)系統(tǒng)發(fā)起攻擊,解決方案的核心原則是過濾這些非法業(yè)務(wù)請(qǐng)求,與是否是熱點(diǎn)數(shù)據(jù)、緩存失效時(shí)間等因素沒有關(guān)系。
緩存擊穿強(qiáng)調(diào)的是熱點(diǎn)key的失效,導(dǎo)致某一時(shí)刻大量請(qǐng)求會(huì)直接到db層,解決方案的核心原則是規(guī)避數(shù)據(jù)庫的并發(fā)操作。緩存雪崩強(qiáng)調(diào)的多個(gè)key的集體失效,與key是否是熱點(diǎn)數(shù)據(jù)并不是必然的因素,解決方案的核心原則則讓key之間的失效時(shí)間分布更加均勻,避免集體失效的情況。
3 數(shù)據(jù)更新場(chǎng)景分析
引入緩存后數(shù)據(jù)會(huì)分別存放到緩存以及數(shù)據(jù)庫兩個(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
這是很通用的更新策略,主要流程如下:
圖片來源:https://coolshell.cn/articles/17416.html
主要涉及到如下幾點(diǎn):
- 失效:應(yīng)用程序先從cache取數(shù)據(jù),沒有得到,則從數(shù)據(jù)庫中取數(shù)據(jù),成功后,放到緩存中。
- 命中:應(yīng)用程序從cache中取數(shù)據(jù),取到后返回。
- 更新:先把數(shù)據(jù)存到數(shù)據(jù)庫中,成功后,再讓緩存失效。
Cache Aside Pattern在數(shù)據(jù)更新的時(shí)候是采用先更新數(shù)據(jù)庫,再失效緩存。為什么需要采用這樣的方式來解決數(shù)據(jù)更新的問題,先假設(shè)更新數(shù)據(jù)庫以及緩存都會(huì)事務(wù)成功,由于某一種更新導(dǎo)致的不一致性在下一章節(jié)進(jìn)行討論。
1)為什么不是更新緩存,而是失效(刪除)緩存?
❶ 并發(fā)寫容易寫覆蓋造成臟數(shù)據(jù)問題:當(dāng)數(shù)據(jù)發(fā)生更新的時(shí)候,針對(duì)緩存數(shù)據(jù)可以有兩種方式來進(jìn)行處理分別是更新緩存數(shù)據(jù)以及失效數(shù)據(jù)讓下一次讀請(qǐng)求重新從db中獲取數(shù)據(jù)后重載入緩存中。假設(shè)更新緩存數(shù)據(jù)的話,在并發(fā)情況下會(huì)存在多線程寫緩存造成臟數(shù)據(jù)的問題,如下圖:
如上圖所示,假設(shè)A、B兩個(gè)線程,A先更新數(shù)據(jù)庫后 B再更新數(shù)據(jù)庫,然后分別進(jìn)行更新緩存,但是B先更新緩存成功,A后更新緩存成功,這樣就導(dǎo)致數(shù)據(jù)庫是最新的數(shù)據(jù)但是緩存中是舊的臟數(shù)據(jù)。而如果失效緩存數(shù)據(jù)的話,可以保證下一次讀請(qǐng)求回源到數(shù)據(jù)庫將最新的數(shù)據(jù)載入到緩存中,避免臟數(shù)據(jù)的問題。因此,針對(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?》。
❷ 雙寫不同數(shù)據(jù)源容易造成數(shù)據(jù)不一致:同時(shí)寫數(shù)據(jù)庫以及緩存數(shù)據(jù),任何一個(gè)更新失敗都會(huì)造成數(shù)據(jù)不一致,由于“物理失敗”造成的數(shù)據(jù)不一致在下一個(gè)章節(jié)進(jìn)行闡述。另外事務(wù)都成功,無論是先更新緩存還是再更新數(shù)據(jù)庫,還是先更新數(shù)據(jù)庫再更新緩存,這兩種情況在并發(fā)的情況下也很容易出現(xiàn)雙寫不成功,操作時(shí)序如下圖,這種方式不推薦。
❸ 先更新緩存再更新數(shù)據(jù)庫
❹ 先更新數(shù)據(jù)庫再更新緩存
❺ 違背數(shù)據(jù)懶加載,避免不必要的計(jì)算消耗:如果有些緩存值是需要經(jīng)過復(fù)雜的計(jì)算才能得出,所以如果每次更新數(shù)據(jù)的時(shí)候都更新緩存,但是后續(xù)在一段時(shí)間內(nèi)并沒有讀取該緩存數(shù)據(jù),這樣就白白浪費(fèi)了大量的計(jì)算性能,完全可以后續(xù)由讀請(qǐng)求的時(shí)候,再去計(jì)算即可,這樣更符合數(shù)據(jù)懶加載,降低計(jì)算開銷。
2)可能存在的更新時(shí)序?
在確定數(shù)據(jù)更新后緩存會(huì)失效來進(jìn)行處理的話,針對(duì)數(shù)據(jù)庫以及緩存更新時(shí)序就存在如下這幾種:
❶ 先失效緩存再更新數(shù)據(jù)庫
❷ 假設(shè)在并發(fā)的情況下,按照這種更新時(shí)序會(huì)存在什么問題?
如時(shí)序圖所示,線程A先失效緩存數(shù)據(jù)的時(shí)候,B線程讀請(qǐng)求發(fā)現(xiàn)緩存數(shù)據(jù)為空的話,就會(huì)從數(shù)據(jù)庫中讀取舊值放入到緩存中,這樣就導(dǎo)致后續(xù)的讀請(qǐng)求讀到的都是緩存中的臟數(shù)據(jù)。針對(duì)這樣的情況可以采用延時(shí)雙刪的策略來有效避免,偽代碼 如下:
- cache.delKey(key);
- db.update(data);
- Thread.sleep(xxx);
- cache.delKey(key);
主要是在寫請(qǐng)求更新完數(shù)據(jù)庫后進(jìn)行休眠一段時(shí)間,然后刪除可能由讀請(qǐng)求帶來的臟數(shù)據(jù)存入到緩存。另外,數(shù)據(jù)庫如果采用的是主從分離的架構(gòu)的話,讀出來的數(shù)據(jù)也有可能是主從未同步完成造成的臟數(shù)據(jù)。這種通過延時(shí)雙刪的方式需要線程休眠,因此很顯然會(huì)降低系統(tǒng)吞吐量,并不是一種優(yōu)雅的解決方式,也可以采用異步刪除的方式。當(dāng)然可以設(shè)置過期時(shí)間,到期后緩存失效載入最新的數(shù)據(jù),需要系統(tǒng)能夠容忍一段時(shí)間的數(shù)據(jù)不一致。
❸ 先更新數(shù)據(jù)庫再失效緩存 :這是推薦的更新數(shù)據(jù)時(shí)采用的方式,實(shí)際上這也是可能存在數(shù)據(jù)不一致的情況,時(shí)序圖如下:
❹ 假設(shè)緩存剛好到期失效時(shí),讀請(qǐng)求從db中讀取數(shù)據(jù),寫請(qǐng)求更新完數(shù)據(jù)后再失效緩存后,讀請(qǐng)求將舊數(shù)據(jù)存入到緩存中,這種情況也會(huì)導(dǎo)致臟數(shù)據(jù)的問題。實(shí)際上這種情況發(fā)生的概率很低,要發(fā)生這種情況的前提條件是寫數(shù)據(jù)庫要先于讀數(shù)據(jù)庫完成,一般而言讀數(shù)據(jù)庫相比于寫數(shù)據(jù)庫要耗時(shí)更短,這種前提條件成立的概率很低。針對(duì)這種”邏輯失敗“造成的數(shù)據(jù)不一致,可以采用上面所說的異步雙刪的策略以及過期失效的方式來避免。
可以看出在并發(fā)的情況下,如果條件苛刻的話,這兩種更新的時(shí)序都有可能導(dǎo)致臟數(shù)據(jù)的情況。只不過在大概率的情況下先更新數(shù)據(jù)庫再失效緩存能夠保證數(shù)據(jù)一致,也是業(yè)界推薦的處理方式,包括Facebook的論文《Scaling Memcache at Facebook》也使用了這個(gè)策略。當(dāng)數(shù)據(jù)發(fā)生變更上,需要考慮的是最新的數(shù)據(jù)放置在哪里?很顯然cache aside pattern 選擇的是將最新的數(shù)據(jù)放到了db上(cache asside pattern:緩存靠邊站),因?yàn)閿?shù)據(jù)不一致的情況大概率會(huì)存在,需要根據(jù)業(yè)務(wù)場(chǎng)景選擇合適的可信設(shè)備存儲(chǔ)最新的數(shù)據(jù)。
3.2 Write/Read Through
Cache Aside Pattern對(duì)db以及緩存的更新邏輯是由調(diào)用方自己去控制,很顯然這是一個(gè)很復(fù)雜的過程。Write/Read Through對(duì)調(diào)用方而言,緩存是作為整個(gè)的數(shù)據(jù)存儲(chǔ),而不用關(guān)系緩存后面的db,數(shù)據(jù)庫的更新則是由緩存統(tǒng)一進(jìn)行管理,對(duì)調(diào)用方而言只需要和緩存進(jìn)行交互,整體過程是透明的。
- Read Through:當(dāng)數(shù)據(jù)發(fā)生更新時(shí),查詢緩存時(shí)更新緩存,然后由緩存層同步的更新數(shù)據(jù)庫即可,對(duì)調(diào)用方而言只需要和緩存層交互即可;
- Write Through:Write Through 套路和Read Through相仿,不過是在更新數(shù)據(jù)時(shí)發(fā)生。當(dāng)有數(shù)據(jù)更新的時(shí)候,如果沒有命中緩存,直接更新數(shù)據(jù)庫,然后返回。如果命中了緩存,則更新緩存,然后再由Cache自己同步更新數(shù)據(jù)庫。如下圖所示(來源于網(wǎng)絡(luò)):
3.3 Write Behind Cache Pattern
這種模式是當(dāng)數(shù)據(jù)更新的時(shí)候直接更新緩存數(shù)據(jù),然后建立異步任務(wù)去更新數(shù)據(jù)庫。這種異步方式請(qǐng)求響應(yīng)會(huì)很快,系統(tǒng)的吞吐量會(huì)明顯提升。但是,因?yàn)槭钱惒礁聰?shù)據(jù)庫,數(shù)據(jù)一致性的保障就會(huì)變?nèi)?,如果更新?shù)據(jù)庫失敗則會(huì)永遠(yuǎn)的造成系統(tǒng)臟數(shù)據(jù),需要很精細(xì)設(shè)計(jì)系統(tǒng)重試的策略,另外如果異步服務(wù)宕機(jī)的話,還要考慮更新的數(shù)據(jù)如何持久化,服務(wù)重啟后能夠迅速恢復(fù)。在更新數(shù)據(jù)庫時(shí),由于并發(fā)多任務(wù)的存在,還需要考慮并發(fā)寫是否會(huì)造成臟數(shù)據(jù)的問題,就需要追溯每次更新數(shù)據(jù)的時(shí)序。使用這種模式需要考慮的細(xì)節(jié)會(huì)有很多,設(shè)計(jì)出一套好的方案是件很不容易的事情。
3.4 更新策略的思考
上面這四種更新策略是非常經(jīng)典的,也是業(yè)界經(jīng)過大規(guī)模業(yè)務(wù)總結(jié)下來的經(jīng)驗(yàn),如果認(rèn)真分析這四種更新策略的話,也會(huì)是受益匪淺,在更新策略的設(shè)計(jì)我得理解是主要關(guān)注如下兩個(gè)方面:
最新的數(shù)據(jù)應(yīng)該放置在哪里?
緩存的存在是為了系統(tǒng)高性能,利用內(nèi)存的IO讀取的高速的特性,來提升系統(tǒng)的性能,提高系統(tǒng)吞吐量,另外,緩存的存在會(huì)讓一部分讀請(qǐng)求不會(huì)到達(dá)db層,分解了db的壓力,畢竟db是最容易出現(xiàn)瓶頸的地方。這是為什么利用緩存的兩個(gè)重要原因。但是,帶來的問題就是,數(shù)據(jù)會(huì)存在在兩個(gè)地方分別是緩存以及數(shù)據(jù)庫中,當(dāng)數(shù)據(jù)更新的時(shí)候就需要思考讓”正確的數(shù)據(jù)應(yīng)該放在哪個(gè)最可信的存儲(chǔ)介質(zhì)上“,就需要結(jié)合業(yè)務(wù)性質(zhì)在兩個(gè)數(shù)據(jù)存儲(chǔ)介質(zhì)上進(jìn)行選擇。
Cache Aside Pattern選擇先更新數(shù)據(jù)庫,再失效緩存,這樣可以保證最新最正確的數(shù)據(jù)一定會(huì)落在數(shù)據(jù)庫中,這樣可以保證核心的業(yè)務(wù)數(shù)據(jù)在數(shù)據(jù)庫中一定是可信的,但是帶來的問題是業(yè)務(wù)邏輯更復(fù)雜,系統(tǒng)處理更新邏輯耗時(shí)更長。如果是非核心數(shù)據(jù)的更新,可以選擇write behind cache pattern的方式,只需要更新緩存即可,能夠快速的響應(yīng)。缺點(diǎn)是很容易造成數(shù)據(jù)不一致,數(shù)據(jù)庫中的數(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í)時(shí)上很難做到數(shù)據(jù)一致,除非采用強(qiáng)一致性方案,這里不在進(jìn)行討論。在找出合適的解決方案前,需要分析下存在數(shù)據(jù)不一致的主要原因,才能對(duì)癥下藥:
1)邏輯失敗造成的數(shù)據(jù)不一致:在上一章主要分析了更新數(shù)據(jù)時(shí)的四種更新策略,在并發(fā)的情況下,無論是先刪除緩存還是更新數(shù)據(jù)庫,還是更新數(shù)據(jù)庫再失效緩存,都會(huì)數(shù)據(jù)不一致的情況,主要是因?yàn)楫惒阶x寫請(qǐng)求在并發(fā)情況下的操作時(shí)序?qū)е碌臄?shù)據(jù)不一致,稱之為”邏輯失敗“。解決這種因?yàn)椴l(fā)時(shí)序?qū)е碌膯栴},核心的解決思想是將異步操作進(jìn)行串行化。
2)物理失敗造成的數(shù)據(jù)不一致:在cache aside pattern中先更新數(shù)據(jù)庫再刪除緩存以及異步雙刪策略等等,如果刪除緩存失敗時(shí)都出現(xiàn)數(shù)據(jù)不一致的情況。但是數(shù)據(jù)庫更新以及緩存操作是沒辦法放到一個(gè)事務(wù)中,一般來說,使用緩存是分布式緩存如果緩存服務(wù)很耗時(shí),那么將更新數(shù)據(jù)庫以及失效緩存放到一個(gè)事務(wù)中,就會(huì)造成大量的數(shù)據(jù)庫連接掛起,嚴(yán)重的降低系統(tǒng)性能,甚至?xí)驗(yàn)閿?shù)據(jù)庫連接數(shù)過多,導(dǎo)致系統(tǒng)崩潰。像這種因?yàn)榫彺娌僮魇?,?dǎo)致的數(shù)據(jù)不一致稱之為”物理失敗“。大多數(shù)情況物理失敗的情況會(huì)重用重試的方式進(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)消息異步刪除緩存
主要流程如下圖所示:
4.2.2 訂閱Binlog
主要流程如下圖所示:
4.2.3 利用隊(duì)列串行化
在分析cache aside pattern發(fā)現(xiàn)在并發(fā)的情況下也會(huì)存在數(shù)據(jù)不一致的場(chǎng)景,只不過發(fā)生的概率很低,另外如果先刪除緩存再更新數(shù)據(jù)庫在并發(fā)讀寫的情況下也會(huì)存在數(shù)據(jù)不一致的情況。類似這種由于并發(fā)時(shí)序?qū)е碌臄?shù)據(jù)不一致的情況,都是因?yàn)閷懻?qǐng)求還沒有結(jié)束讀請(qǐng)求讀取的是舊數(shù)據(jù),如果讀請(qǐng)求在寫請(qǐng)求之后處理,即請(qǐng)求的處理能夠串行化的話,就能保證讀請(qǐng)求讀到的是寫請(qǐng)求更新的最新的數(shù)據(jù)。
將請(qǐng)求進(jìn)行串行化,最常用的方式是采用隊(duì)列的方式,一個(gè)隊(duì)列只能對(duì)應(yīng)一個(gè)工作線程,更新數(shù)據(jù)的寫請(qǐng)求放置隊(duì)列中,等待異步處理;讀請(qǐng)求如果能從緩存中獲取數(shù)據(jù),則返回,如果緩存中沒有數(shù)據(jù),就將讀請(qǐng)求放置到隊(duì)列中,等待寫請(qǐng)求數(shù)據(jù)更新完成。這種方案需要考慮的問題有:
1)讀請(qǐng)求長時(shí)間阻塞:如果隊(duì)列中擠壓了多個(gè)寫請(qǐng)求,則讀請(qǐng)求會(huì)存在長時(shí)間阻塞的情況,需要設(shè)置超時(shí)處理策略,一旦超過超時(shí)時(shí)間,則直接讀取數(shù)據(jù)庫返回,避免長時(shí)間不響應(yīng);另外,在業(yè)務(wù)中需要進(jìn)行壓測(cè),考慮隊(duì)列中在峰值情況下會(huì)積攢多少寫請(qǐng)求,如果過多,需要考慮隊(duì)列優(yōu)化的方式和相應(yīng)的解決方案;
2)多個(gè)隊(duì)列分散壓力:可以根據(jù)數(shù)據(jù)項(xiàng)通過hash等路由方式,創(chuàng)建多個(gè)隊(duì)列并行執(zhí)行來提升系統(tǒng)吞吐量;
3)操作復(fù)雜需要考慮全面:由于采用隊(duì)列來進(jìn)行串行化,那么要考慮隊(duì)列的可用性,隊(duì)列阻塞以及服務(wù)掛掉后的容災(zāi)恢復(fù)策略是否健壯等等,相對(duì)而言整體的方案需要考慮的點(diǎn)會(huì)有很多;
這種方式可以做到數(shù)據(jù)強(qiáng)一致性,由于串行化系統(tǒng)的吞吐量會(huì)下降很多并且操作復(fù)雜,畢竟任何方案都會(huì)有利弊權(quán)衡的過程,需要根據(jù)業(yè)務(wù)場(chǎng)景選擇合適的技術(shù)方案。針對(duì)數(shù)據(jù)強(qiáng)一致性很有很多方案,但基本上操作設(shè)計(jì)都很復(fù)雜,在大多數(shù)業(yè)務(wù)場(chǎng)景滿足數(shù)據(jù)最終一致性即可。
當(dāng)然除了以上這三種通用的方法外,為緩存設(shè)置過期時(shí)間以及定時(shí)全量同步,也是接近最終一致性的最簡單以及有效的方式。
5. 常見的幾個(gè)場(chǎng)景問題
在分析數(shù)據(jù)更新的策略后發(fā)現(xiàn)正確使用緩存是一件很不容易的事情,在實(shí)際使用緩存時(shí),還會(huì)有很多有意思的場(chǎng)景(”坑“),在這里進(jìn)行一下總結(jié):
1)過期還是不過期緩存數(shù)據(jù):針對(duì)緩存數(shù)據(jù)是否需要設(shè)置過期時(shí)間也需要結(jié)合場(chǎng)景來進(jìn)行分析,一些長尾商品,大多數(shù)數(shù)據(jù)在業(yè)務(wù)中都是讀場(chǎng)景更多,并且緩存空間很大的話,就可以考慮不過期數(shù)據(jù)。那是否就意味著這就是一份靜態(tài)數(shù)據(jù)了?當(dāng)緩存空間已滿時(shí),數(shù)據(jù)會(huì)根據(jù)淘汰策略移除緩存,另外數(shù)據(jù)更新時(shí)也可以通過Binlog等其他方式進(jìn)行異步失效緩存。
如果系統(tǒng)通過消息異步更新操作成本過高或者依賴于外部系統(tǒng)無法進(jìn)行訂閱binlog異步更新的話,就需要來采用過期緩存數(shù)據(jù)來保障數(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的問題要時(shí)刻警惕,可以考慮將value進(jìn)行壓縮,以及緩存時(shí)進(jìn)行拆解,然后在業(yè)務(wù)服務(wù)中進(jìn)行數(shù)據(jù)聚合來避免大value的問題;
4)熱點(diǎn)緩存問題:針對(duì)熱點(diǎn)數(shù)據(jù)如果每次都從遠(yuǎn)程緩存去獲取,會(huì)給緩存系統(tǒng)帶來過多的負(fù)載,會(huì)導(dǎo)致獲取緩存數(shù)據(jù)響應(yīng)過慢,可以使用緩存集群,掛載更多的從緩存,讀取數(shù)據(jù)從從緩存中獲取。針對(duì)熱點(diǎn)數(shù)據(jù)可以使用應(yīng)用本地緩存來減少對(duì)遠(yuǎn)程緩存的請(qǐng)求負(fù)載;
5)數(shù)據(jù)預(yù)熱:可以預(yù)先將數(shù)據(jù)加載到緩存中,方式緩存數(shù)據(jù)為空,大量的請(qǐng)求回源到db。如果容量很高可以考慮全量預(yù)熱,如果容量優(yōu)先,就只能選擇高頻熱點(diǎn)數(shù)據(jù)進(jìn)行數(shù)據(jù)預(yù)熱,還需要關(guān)注是否有批量操作以及慢sql帶來的性能問題,在整個(gè)數(shù)據(jù)預(yù)熱過程中需要有可靠的監(jiān)控機(jī)制來保障;
6)非預(yù)期熱點(diǎn)數(shù)據(jù):針對(duì)業(yè)務(wù)預(yù)估不足的熱點(diǎn)數(shù)據(jù),需要有熱點(diǎn)發(fā)現(xiàn)系統(tǒng)來統(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í)例可以使用,但是這種方式的問題在于水平擴(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ù)載過高成為系統(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ì)有很多大大小小的影響因素,比如語言本身的特性的影響,例如Java需要考慮GC的影響。還需要盡可能的提升緩存命中率等等多個(gè)方面,總結(jié)下來,核心的幾個(gè)影響因素如下:
1)提升緩存命中率:影響緩存命中率的幾個(gè)因素:
❶ 業(yè)務(wù)時(shí)效性要求:緩存適合"讀多寫少"的業(yè)務(wù)場(chǎng)景,并且業(yè)務(wù)性質(zhì)決定了時(shí)效性要求,不同的時(shí)效性要求決定了緩存的更新策略以及過期時(shí)間,對(duì)時(shí)效性也低的業(yè)務(wù)越適合使用緩存,并且緩存命中率越高;
❷ 緩存粒度設(shè)計(jì):通常而言,緩存對(duì)象粒度越小就越適合使用緩存,不會(huì)導(dǎo)致頻繁更新導(dǎo)致緩存命中率下降;
❸ 緩存淘汰策略:如果緩存空間有限,不同的緩存淘汰策略也會(huì)影響緩存命中率,如果淘汰的緩存數(shù)據(jù)后續(xù)被大量使用,無疑就會(huì)降低緩存命中率;
❹ 緩存部署方式:在使用分布式緩存時(shí),要做好容量規(guī)劃以及容災(zāi)策略,方式緩存實(shí)例故障后造成大規(guī)模緩存失效;
❺ Key路由策略:不同路由策略會(huì)在節(jié)點(diǎn)實(shí)例故障后帶來不同的影響,如果采用取模的方式水平擴(kuò)展時(shí)則會(huì)降低緩存命中率。通過這些分析,提高緩存命中率沒有放之四海而皆準(zhǔn)的統(tǒng)一規(guī)則,需要從這些角度去思考,盡可能的在高頻訪問且時(shí)效性不是很高的業(yè)務(wù)數(shù)據(jù)上使用緩存。
2)序列化方式:使用遠(yuǎn)程緩存服務(wù)免不了需要經(jīng)過序列化后在網(wǎng)絡(luò)中進(jìn)行數(shù)據(jù)傳輸,那么選擇不同的序列化方式對(duì)緩存性能會(huì)有影響。選擇序列化方式時(shí)需要考慮序列化耗時(shí)、序列化后在網(wǎng)絡(luò)傳輸中包大小以及序列化的計(jì)算開銷。
3)GC影響:采用多級(jí)緩存以及大value時(shí)會(huì)采用應(yīng)用本地緩存,對(duì)于java應(yīng)用,就需要考慮大對(duì)象帶來的GC影響。
4)緩存協(xié)議:了解不同的緩存協(xié)議的優(yōu)缺點(diǎn)比如Redis以及Memcached協(xié)議,根據(jù)業(yè)務(wù)場(chǎng)景進(jìn)行選擇。
5)緩存連接池:為提升訪問性能,需要合理的設(shè)置緩存連接池。
6)完善的監(jiān)控平臺(tái):需要考慮是否有一套緩存的監(jiān)控平臺(tái),能夠追蹤緩存使用情況、緩存服務(wù)整體的性能以及一些非預(yù)期熱點(diǎn)數(shù)據(jù)的發(fā)現(xiàn)策略等等,這樣才能綜合整體的保障緩存服務(wù)的可用以及性能。
7. 多級(jí)緩存設(shè)計(jì)案例
從用戶發(fā)出請(qǐng)求到到最底層的數(shù)據(jù)庫實(shí)際上會(huì)經(jīng)歷很多節(jié)點(diǎn),因此在整個(gè)鏈路上都可以設(shè)置緩存,并且按照緩存最近原則將緩存放置在里用戶最近的地方提升系統(tǒng)響應(yīng)的效果最為明顯,相應(yīng)的提升系統(tǒng)吞吐量的效果就越為顯著,通過能夠大大降低對(duì)后端的壓力。在整個(gè)鏈路流程里可以添加緩存的地方有:發(fā)起請(qǐng)求-->瀏覽器/客戶端緩存-->邊緣緩存/CDN-->反向代理(Nginx)緩存-->遠(yuǎn)程緩存-->進(jìn)程內(nèi)緩存-->數(shù)據(jù)庫緩存。服務(wù)端多級(jí)緩存設(shè)計(jì)通用的技術(shù)方案如下:
主要流程為:
1)請(qǐng)求先達(dá)到Nginx,先讀取Nginx本地緩存,如果命中緩存則返回緩存數(shù)據(jù)。這里的負(fù)載均衡路由策略,采用輪詢的方式相對(duì)而言訪問壓力分布的更加均衡,一致性哈希方式能夠提升緩存命中率,但是同時(shí)也會(huì)存在單點(diǎn)壓力過大的問題,可以考慮使用一致性哈希策略時(shí)流量達(dá)到一定閾值的時(shí)候切換成輪詢的方式;
2)如果沒有命中Nginx緩存,則讀取分布式緩存,為了高可用以及提升系統(tǒng)吞吐量,一般遠(yuǎn)程分布式緩存會(huì)采用主從結(jié)構(gòu),這里讀取的就是從緩存服務(wù)集群數(shù)據(jù),如果命中緩存則返回?cái)?shù)據(jù);
3)如果從緩存沒有命中緩存,則讀取應(yīng)用本地緩存(堆內(nèi)/堆外緩存),這里的路由策略同樣可以采用輪詢或者一致性哈希。如果命中,則返回?cái)?shù)據(jù),并回寫到Nginx緩存中;為避免由于從緩存服務(wù)出現(xiàn)問題,造成過大的流量沖垮數(shù)據(jù)庫,這里可以嘗試讀取主緩存服務(wù);
4)如果所有緩存沒有命中,則查詢數(shù)據(jù)庫并返回?cái)?shù)據(jù),并異步回寫到主緩存以及應(yīng)用本地緩存中。主緩存通過主從同步機(jī)制同步到從緩存服務(wù)集群中。這里會(huì)寫到主緩存的時(shí)候需要考慮多個(gè)應(yīng)用實(shí)例在異步寫,需要考慮數(shù)據(jù)是否會(huì)亂序的問題。
另外,對(duì)于一些非預(yù)期熱點(diǎn)數(shù)據(jù)比如微博中”某某明星結(jié)婚“等等熱門話題帶來的訪問流量瞬間沖擊到后端,針對(duì)以上多級(jí)緩存設(shè)計(jì),可以通過引入熱點(diǎn)發(fā)現(xiàn)系統(tǒng)來發(fā)現(xiàn)非預(yù)期的熱點(diǎn)數(shù)據(jù),利用flume訂閱Nginx日志,然后通過消息進(jìn)行消費(fèi),最后通過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ì)如下:
8. 總結(jié)
為了追求高性能,每個(gè)開發(fā)者最先使用的就是緩存,也在潛意識(shí)里將緩存作為了系統(tǒng)性能瓶頸的一劑良藥,經(jīng)過系統(tǒng)化的總結(jié)和分析緩存后,就可以發(fā)現(xiàn)緩存如果使用不當(dāng)真的就會(huì)事與愿違,成為毒藥,并不會(huì)系統(tǒng)迭代出那個(gè)局部最優(yōu)解。如果貿(mào)然的使用緩存,需要考慮的地方真的很多稍有不注意,反而會(huì)讓系統(tǒng)投入更多的維護(hù)成本,陡增更高的復(fù)雜度。那是不是就不使用緩存呢?也不是,緩存在高并發(fā)的情況下通過IO高速的緩存獲取數(shù)據(jù)能使得每個(gè)請(qǐng)求能夠快速響應(yīng),并且能夠大大提升系統(tǒng)吞吐量以及支撐更高的并發(fā)用戶數(shù),在現(xiàn)有的高并發(fā)大流量的互聯(lián)網(wǎng)應(yīng)用中應(yīng)用緩存的例子太多了,也足以證明緩存在優(yōu)化系統(tǒng)整體性能是一種行之有效的方案。
作為開發(fā)者不是每個(gè)人都有機(jī)會(huì)和機(jī)遇去挑戰(zhàn)高并發(fā)的互聯(lián)網(wǎng)架構(gòu)以及高量級(jí)的訪問流量和應(yīng)用規(guī)模的,那是不是就意味著這些通用的技術(shù)方案就不用深刻分析呢?很顯然不是,單從緩存使用中就會(huì)發(fā)現(xiàn)在高并發(fā)下讀寫帶來的數(shù)據(jù)不一致性分析下來就會(huì)有很多并發(fā)場(chǎng)景,單線程下都是正常的,但在并發(fā)下就會(huì)出現(xiàn)很多意想不到的case,而這些分析的思路是最核心的,也是開發(fā)者逐漸形成自己的方法論的有效訓(xùn)練途徑。在系統(tǒng)化學(xué)習(xí)每一種技術(shù)組件時(shí),業(yè)界的通用解決方案都是經(jīng)過歷史經(jīng)驗(yàn)慢慢沉淀下來的智慧,如同品酒,是需要靜下心來好好去品的。
技術(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)常所說的PPT架構(gòu)師,PPT很炫酷,一頓操作猛如虎但是并不是最適合業(yè)務(wù)的那個(gè)解,反而就像是跳梁小丑一樣自嗨或者帶著功利心去急于變現(xiàn),只有業(yè)務(wù)與技術(shù)結(jié)合能夠得到最大價(jià)值的那個(gè)解就是最合適的方案,需要在優(yōu)與劣的trade-off上做出權(quán)衡。如果很懂技術(shù),但是不懂業(yè)務(wù),同樣的就是廢銅爛鐵沒辦法發(fā)揮出功力。在不同的職業(yè)生涯階段,每個(gè)人的精力有限,投入技術(shù)以及業(yè)務(wù)的精力分配也是不同的,專注的點(diǎn)會(huì)有所不同,就像業(yè)務(wù)與技術(shù)一樣,在人生的賽道中在不同階段也需要迭代出那個(gè)最合適的局部最優(yōu)解,至于什么最合適,答案在每個(gè)人心中!