翻越缓存的三座大山
前言
在互聯(lián)網(wǎng)和移動(dòng)互聯(lián)網(wǎng)兩波浪潮的推動(dòng)下,存儲(chǔ)技術(shù)有了飛速發(fā)展。移動(dòng)互聯(lián)網(wǎng)用戶在過(guò)去十年增長(zhǎng)了 10 倍,用戶的增長(zhǎng)帶動(dòng)了數(shù)據(jù)量的指數(shù)級(jí)增長(zhǎng),因?yàn)榧ち业氖袌?chǎng)競(jìng)爭(zhēng),企業(yè)和用戶對(duì)應(yīng)用程序的響應(yīng)性能要求越來(lái)越高,在完美應(yīng)對(duì)龐大的用戶規(guī)模和海量數(shù)據(jù)集的同時(shí)保證優(yōu)秀的產(chǎn)品體驗(yàn),是數(shù)據(jù)庫(kù)面臨的挑戰(zhàn)。在機(jī)械硬盤普及的時(shí)代,企業(yè)需要通過(guò)緩存技術(shù)加速數(shù)據(jù)的訪問(wèn),在 SSD 存儲(chǔ)介質(zhì)普及后,企業(yè)需要緩存技術(shù)支撐高并發(fā)和大吞吐,通過(guò)引入分布式緩存方案,提升應(yīng)用程序性能,消除數(shù)據(jù)庫(kù)熱點(diǎn)。但是緩存技術(shù)的引入增加了業(yè)務(wù)架構(gòu)的復(fù)雜度,降低了開發(fā)效率,同時(shí)還面臨著緩存一致性、緩存擊穿、緩存雪崩等挑戰(zhàn)。
緩存的三座大山
緩存一致性
緩存一致性是指業(yè)務(wù)在引入分布式緩存系統(tǒng)后,業(yè)務(wù)對(duì)數(shù)據(jù)的更新除了要更新存儲(chǔ)以外還需要同時(shí)更新緩存,對(duì)兩個(gè)系統(tǒng)進(jìn)行數(shù)據(jù)更新就要先解決分布式系統(tǒng)中的隔離性和原子性難題。目前大多數(shù)業(yè)務(wù)在引入分布式緩存后都是通過(guò)犧牲小概率的一致性來(lái)保障業(yè)務(wù)性能,因?yàn)橐跇I(yè)務(wù)層嚴(yán)格保障數(shù)據(jù)的一致性,代價(jià)非常高,業(yè)務(wù)引入分布式緩存主要是為了解決性能問(wèn)題,所以在性能和一致性面前,通常選擇犧牲小概率的一致性來(lái)保障業(yè)務(wù)性能。
緩存擊穿
緩存擊穿是指查詢請(qǐng)求沒(méi)有在緩存層命中而將查詢透?jìng)鞯酱鎯?chǔ) DB 的問(wèn)題,當(dāng)大量的請(qǐng)求發(fā)生緩存擊穿時(shí),將給存儲(chǔ) DB 帶來(lái)極大的訪問(wèn)壓力,甚至導(dǎo)致 DB 過(guò)載拒絕服務(wù)??諗?shù)據(jù)查詢(黑客攻擊)和緩存污染(網(wǎng)絡(luò)爬蟲)是常見的引發(fā)緩存擊穿的原因。什么是空數(shù)據(jù)查詢?空數(shù)據(jù)查詢通常指攻擊者偽造大量不存在的數(shù)據(jù)進(jìn)行訪問(wèn)(比如不存在的商品信息、用戶信息)。緩存污染通常指在遍歷數(shù)據(jù)等情況下冷數(shù)據(jù)把熱數(shù)據(jù)驅(qū)逐出內(nèi)存,導(dǎo)致緩存了大量冷數(shù)據(jù)而熱數(shù)據(jù)被驅(qū)逐。緩存污染的場(chǎng)景我們目前還沒(méi)有發(fā)現(xiàn)較好的解決方案,但是在空數(shù)據(jù)查詢問(wèn)題上我們可以改造業(yè)務(wù),通過(guò)以下方式防止緩存擊穿:
通過(guò) bloomfilter 記錄 key 是否存在,從而避免無(wú)效 Key 的查詢;
在 Redis 緩存不存在的 Key,從而避免無(wú)效 Key 的查詢;
緩存雪崩
緩存雪崩是指由于大量的熱數(shù)據(jù)設(shè)置了相同或接近的過(guò)期時(shí)間,導(dǎo)致緩存在某一時(shí)刻密集失效,大量請(qǐng)求全部轉(zhuǎn)發(fā)到 DB,或者是某個(gè)冷數(shù)據(jù)瞬間涌入大量訪問(wèn),這些查詢?cè)诰彺?MISS 后,并發(fā)的將請(qǐng)求透?jìng)鞯?DB,DB 瞬時(shí)壓力過(guò)載從而拒絕服務(wù)。目前常見的預(yù)防緩存雪崩的解決方案,主要是通過(guò)對(duì) key 的 TTL 時(shí)間加隨機(jī)數(shù),打散 key 的淘汰時(shí)間來(lái)盡量規(guī)避,但是不能徹底規(guī)避。
傳統(tǒng)分布式緩存方案
在引入分布式緩存后,我們的業(yè)務(wù)架構(gòu)由原有兩層架構(gòu)(應(yīng)用+數(shù)據(jù)庫(kù))變成了三層架構(gòu)(應(yīng)用+緩存+存儲(chǔ)),緩存層緩存熱數(shù)據(jù),存儲(chǔ)層負(fù)責(zé)全量數(shù)據(jù)持久化存儲(chǔ)。存儲(chǔ)架構(gòu)的變化要求業(yè)務(wù)對(duì)數(shù)據(jù)的存取邏輯進(jìn)行相應(yīng)調(diào)整,而且這個(gè)調(diào)整是巨大的。在緩存系統(tǒng)的選擇上,常見的緩存數(shù)據(jù)庫(kù)包括 Memcached、Redis,目前使用最廣泛的是 Redis,存儲(chǔ)數(shù)據(jù)常見的包括關(guān)系型數(shù)據(jù)庫(kù) MySQL、PG、Oreacle、SQLServer 等,NoSQL 數(shù)據(jù)庫(kù) MongoDB、Hbase 等。在引入分布式緩存后,業(yè)務(wù)邏輯需要做三個(gè)點(diǎn)的變化,緩存讀取、緩存更新、緩存淘汰。
緩存讀取
引入緩存層后,讀數(shù)據(jù)就變得不是那么簡(jiǎn)單直接了,APP 需要先去緩存讀取數(shù)據(jù),如果緩存 MISS(數(shù)據(jù)沒(méi)有被緩存),則需要從存儲(chǔ)中讀取數(shù)據(jù),并將數(shù)據(jù)更新到緩存系統(tǒng)中,整個(gè)流程和代碼如下所示:
示例代碼
緩存更新
我們把常見的緩存更新方案總結(jié)為兩大類,業(yè)務(wù)層更新和外部組件更新,比較常見的是通過(guò)業(yè)務(wù)更新的方案。
業(yè)務(wù)層更新緩存
緩存更新的難點(diǎn)
剛開始接觸緩存方案的同學(xué)可能會(huì)糾結(jié)幾個(gè)點(diǎn),先更新緩存還是先更新存儲(chǔ),緩存的處理是通過(guò)刪除來(lái)實(shí)現(xiàn)還是通過(guò)更新來(lái)實(shí)現(xiàn)。這里我們面臨的問(wèn)題本質(zhì)上是一個(gè)數(shù)據(jù)庫(kù)的分布式事務(wù)的問(wèn)題,需要處理數(shù)據(jù)可靠性的挑戰(zhàn),并發(fā)更新帶來(lái)的隔離性挑戰(zhàn),和數(shù)據(jù)更新原子性的挑戰(zhàn)。
數(shù)據(jù)可靠性
如果要保證數(shù)據(jù)的可靠性,在業(yè)務(wù)邏輯成功之前,必須保障有一份數(shù)據(jù)落地,我們有以下兩個(gè)選擇:
先更新成功存儲(chǔ),再更新緩存;
先更新成功緩存,再跟新存儲(chǔ),如果存儲(chǔ)更新失敗,刪除緩存;
操作隔離性。
一條數(shù)據(jù)的更新涉及到存儲(chǔ)和緩存兩套系統(tǒng),如果多個(gè)線程同時(shí)操作一條數(shù)據(jù),并且沒(méi)有方案保證多個(gè)操作之間的有序執(zhí)行,就可能會(huì)發(fā)生更新順序錯(cuò)亂導(dǎo)致數(shù)據(jù)不一致的問(wèn)題。
更新原子性
引入緩存后,我們需要保證緩存和存儲(chǔ)要么同時(shí)更新成功,要么同時(shí)更新失敗,否則部分更新成功就會(huì)導(dǎo)致緩存和存儲(chǔ)數(shù)據(jù)不一致的問(wèn)題。
業(yè)務(wù)層緩存更新方案
我們看到大多數(shù)的常見是選擇以下方案,保障數(shù)據(jù)可靠性,盡量減少數(shù)據(jù)不一致的出現(xiàn),通過(guò) TTL 超時(shí)機(jī)制在一定時(shí)間段后自動(dòng)解決數(shù)據(jù)不一致現(xiàn)象。
Step1:更新存儲(chǔ),保證數(shù)據(jù)可靠性;
Step2:更新緩存,2 個(gè)策略怎么選:
惰性更新:刪除緩存,等待下次讀 MISS 再緩存(推薦方案);
積極更新:將最新的值更新到緩存(不推薦);
積極更新策略,緩存數(shù)據(jù)實(shí)時(shí)性更高,但是在緩存?zhèn)葞?lái)了更多的更新操作,這會(huì)提高更新沖突導(dǎo)致臟數(shù)據(jù)概率。
外部組件更新緩存
緩存 MISS 處理方案
在通過(guò)第三方組件更新的方案中,為了保障數(shù)據(jù)的一致性,避免對(duì)單條數(shù)據(jù)的并行更新,緩存的所有更新操作都需要交給同步組件,因此緩存 MISS 場(chǎng)景下的邏輯:
緩存更新方案
第一:需要監(jiān)控存儲(chǔ)的日志,或者通過(guò) Triger 來(lái)監(jiān)控存儲(chǔ)數(shù)據(jù)的變更,需要對(duì)存儲(chǔ)系統(tǒng)非常熟悉;
第二:需要對(duì)更新進(jìn)行過(guò)濾,我們的目的是緩存熱數(shù)據(jù),但是像 DDL、批量更新這一系列的操作是不需要更新緩存的,要把非業(yè)務(wù)更新操作過(guò)濾;
第三:同步組件需要理解數(shù)據(jù),不通用;
先更新存儲(chǔ),由第三方組件異步更新緩存;
該方案投入較大,只適合特定的場(chǎng)景,并且有以下 3 個(gè)難點(diǎn):
其他緩存更新方案
在實(shí)際的生產(chǎn)中,我們還會(huì)看到很多先更新緩存,然后通過(guò)第三方組件更新存儲(chǔ)的場(chǎng)景,但是這個(gè)方案也會(huì)面臨數(shù)據(jù)一致性和數(shù)據(jù)可靠性的挑戰(zhàn),雖然不推薦,但是確實(shí)還是能看到有在使用這個(gè)方案的,我們拿出來(lái)探討下。
這個(gè)場(chǎng)景數(shù)據(jù)可靠性,不及先更新存儲(chǔ)的方案,但是寫入性能高,延遲低;
這個(gè)方案 APP 和第三方組件都會(huì)更新 Cache,會(huì)存在數(shù)據(jù)一致性的問(wèn)題,因?yàn)楹茈y保障兩個(gè)組件更新的時(shí)序。
緩存淘汰
緩存的作用是將熱點(diǎn)數(shù)據(jù)緩存到內(nèi)存實(shí)現(xiàn)加速,內(nèi)存的成本要遠(yuǎn)高于磁盤,因此我們通常僅僅緩存熱數(shù)據(jù)在內(nèi)存,冷數(shù)據(jù)需要定期的從內(nèi)存淘汰,數(shù)據(jù)的淘汰通常有兩種方案:
主動(dòng)淘汰,這是推薦的方式,我們通過(guò)對(duì) Key 設(shè)置 TTL 的方式來(lái)讓 Key 定期淘汰,以保障冷數(shù)據(jù)不會(huì)長(zhǎng)久的占有內(nèi)存。TTL 的策略可以保證冷數(shù)據(jù)一定被淘汰,但是沒(méi)有辦法保障熱數(shù)據(jù)始終在內(nèi)存,這個(gè)我們?cè)诤竺鏁?huì)展開;
被動(dòng)淘汰,這個(gè)是保底方案,并不推薦,Redis 提供了一系列的 Maxmemory 策略來(lái)對(duì)數(shù)據(jù)進(jìn)行驅(qū)逐,觸發(fā)的前提是內(nèi)存要到達(dá) maxmemory(內(nèi)存使用率 100%),在 maxmemory 的場(chǎng)景下緩存的質(zhì)量是不可控的,因?yàn)槊看尉彺嬉粋€(gè) Key 都可能需要去淘汰一個(gè) Key。
- END -
看完一鍵三連在看,轉(zhuǎn)發(fā),點(diǎn)贊
是對(duì)文章最大的贊賞,極客重生感謝你
推薦閱讀
深入理解數(shù)據(jù)結(jié)構(gòu)和算法
深入理解Kafka的設(shè)計(jì)思想
深入理解RCU|核心原理
總結(jié)
- 上一篇: 深入理解RCU|核心原理
- 下一篇: JVM底层原理解析