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