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