1.應用場景
(1) 緩存
緩存機制幾乎在所有的大型網站都有使用,合理地使用緩存不僅可以加快數據的訪問速度,而且能夠有效地降低后端數據源的壓力。Redis 提供了鍵值過期時間設置,並且也提供了靈活控制最大內存和內存溢出后的淘汰策略。可以這么說,一個合理的緩存設計能夠為一個網站的穩定保駕護航。
(2) 排行榜系統
排行榜系統幾乎存在於所有的網站,例如按照熱度排名的排行榜,按照發布時間的排行榜,按照各種復雜維度計算出的排行榜, Redis 提供了列表和有序集合數據結構,合理地使用這些數據結構可以很方便地構建各種排行榜系統。
(3) 計數器應用
計數器在網站中的作用至關重要,例如視頻網站有播放數、電商網站有瀏覽數,為了保證數據的實時性,每一次播放和瀏覽都要做加1 的操作,如果並發量很大對千傳統關系型數據的性能是一種挑戰。Redis 天然支持計數功能而且計數的性能也非常好,可以說是計數器系統的重要選擇。
(4) 社交網絡
贊/踩、粉絲、共同好友/喜好、推送、下拉刷新等是社交網站的必備功能,由於社交網站訪問量通常比較大,而且傳統的關系型數據不太適合保存這種類型的數據, Redis 提供的數據結構可以相對比較容易地實現這些功能。
(5) 消息隊列系統
消息隊列系統可以說是一個大型網站的必備基礎組件,因為其具有業務解耦、非實時業務削峰等特性。Redis 提供了發布訂閱功能和阻塞隊列的功能,雖然和專業的消息隊列比還不夠足夠強大,但是對於一般的消息隊列功能基本可以滿足。
2.使用緩存的收益和成本
如圖左側為客戶端直接調用存儲層的架構,右側為比較典型的緩存層+存儲層架構,下面分析一下緩存加入后帶來的收益和成本。
收益:
l 加速讀寫:因為緩存通常都是全內存的(例如Redis、Memcache),而存儲層通常讀寫性能不夠強悍(例如MySQL),通過緩存的使用可以有效地加速讀寫,優化用戶體驗。
l 降低后端負載:幫助后端減少訪問量和復雜計算(例如很復雜的SQL語句),在很大程度降低了后端的負載。
成本:
l 數據不一致性:緩存層和存儲層的數據存在着一定時間窗口的不一致性,時間窗口跟更新策略有關。
l 代碼維護成本:加入緩存后,需要同時處理緩存層和存儲層的邏輯,增大了開發者維護代碼的成本。
l 運維成本:以Redis Cluster為例,加入后無形中增加了運維成本。
3. 緩存問題
3.1 緩存穿透
3.1.1 問題描述
緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,通常出於容錯的考慮,如果從存儲層查不到數據則不寫入緩存層,如下圖所示
整個過程分為如下3步:
1) 緩存層不命中。
2) 存儲層不命中,不將空結果寫回緩存。
3) 返回空結果。
緩存穿透將導致不存在的數據每次請求都要到存儲層去查詢,失去了緩存保護后端存儲的意義。
緩存穿透問題可能會使后端存儲負載加大,由於很多后端存儲不具備高並發性,甚至可能造成后端存儲宕掉。通常可以在程序中分別統計總調用數、緩存層命中數、存儲層命中數,如果發現大量存儲層空命中,可能就是出現了緩存穿透問題。
3.1.2解決方案
造成緩存穿透的基本原因有兩個。第一,自身業務代碼或者數據出現問題,第二,一些惡意攻擊、爬蟲等造成大量空命中。下面我們來看一下如何解決緩存穿透問題。
(1) 緩存空對象
如圖所示,當第2步存儲層不命中后,仍然將空對象保留到緩存層中,之后再訪問這個數據將會從緩存中獲取,這樣就保護了后端數據源。
緩存空對象會有兩個問題:第一,空值做了緩存,意味着緩存層中存了更多的鍵,需要更多的內存空間,比較有效的方法是針對這類數據設置一個較短的過期時間,讓其自動剔除。第二,緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有一定影響。例如過期時間設置為5分鍾,如果此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時可以利用消息系統或者其他方式清除掉緩存層中的空對象。
(2) 布隆過濾器攔截
布隆過濾器:實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都比一般的算法要好的多,缺點是有一定的誤識別率和刪除困難。可以告訴你某樣東西一定不存在或者可能存在。
如圖所示,在訪問緩存層和存儲層之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截。例如:一個推薦系統有4億個用戶id,每個小時算法工程師會根據每個用戶之前歷史行為計算出推薦數據放到存儲層中,但是最新的用戶由於沒有歷史行為,就會發生緩存穿透的行為,為此可以將所有推薦數據的用戶做成布隆過濾器。如果布隆過濾器認為該用戶id不存在,那么就不會訪問存儲層,在一定程度保護了存儲層。
(3) 兩種方案比對
解決緩存穿透 |
適用場景 |
維護成本 |
緩存空對象 |
l 數據命中不高 l 數據頻繁變化實時性高 |
l 代碼維護簡單 l 需要過多的緩存空間 l 數據不一致 |
布隆過濾器 |
l 數據命中不高 l 數據相對固定實時性低 |
l 代碼維護復雜 l 緩存空間占用少 |
3.2 緩存雪崩
如圖描述了什么是緩存雪崩:由於緩存層承載着大量請求,有效地保護了存儲層,但是如果緩存層由於某些原因不能提供服務,於是所有的請求都會達到存儲層,存儲層的調用量會暴增,造成存儲層也會級聯宕機的情況。
預防和解決緩存雪崩問題,可以從以下三個方面進行着手。
(1) 保證緩存層服務高可用性。如果緩存層設計成高可用的,即使個別節點、個別機器、甚至是機房宕掉,依然可以提供服務,例如前面介紹過的Redis Sentinel和Redis Cluster都實現了高可用。
(2) 依賴隔離組件為后端限流並降級。無論是緩存層還是存儲層都會有出錯的概率,可以將它們視同為資源。作為並發量較大的系統,假如有一個資源不可用,可能會造成線程全部阻塞在這個資源上,造成整個系統不可用。降級機制在高並發系統中是非常普遍的。實際項目中,我們需要對重要的資源(例如Redis、MySQL、HBase、外部接口)都進行隔離,讓每種資源都單獨運行在自己的線程池中,即使個別資源出現了問題,對其他服務沒有影響。但是線程池如何管理,比如如何關閉資源池、開啟資源池、資源池閥值管理,這些做起來還是相當復雜的。這里推薦使用Java依賴隔離工具Hystrix,他是解決依賴隔離的利器。
(3) 提前演練。在項目上線前,演練緩存層宕掉后,應用以及后端的負載情況以及可能出現的問題,在此基礎上做一些預案設定。
3.3 緩存擊穿(熱點數據集中失效)
3.3.1 問題描述
當一個key是熱點key,並發量很大,而且重建緩存不能在短時間完成,在緩存失效的一瞬間,就會有大量的線程來重建緩存,造成后端負載加大,甚至讓應用崩潰,這就叫緩存擊穿。如下圖:
3.3.2 解決方案
(1) 互斥鎖
此方法只允許一個線程重建緩存,其他線程等待重建緩存的線程執行完,重新從緩存獲取數據即可,整個過程如圖所示。
(2) 永遠不過期
“永遠不過期”包含兩層意思:
l 從緩存層面來看,確實沒有設置過期時間,所以不會出現熱點key過期后產生的問題,也就是“物理”不過期。
l 從功能層面來看,為每個value設置一個邏輯過期時間,當發現超過邏輯過期時間后,會使用單獨的線程去構建緩存。
整個過程如圖所示:
此方法有效杜絕了熱點key產生的問題,但唯一不足的就是重構緩存期間,會出現數據不一致的情況,這取決於應用方是否容忍這種不一致。
(3) 兩種方案對比
解決方案 |
優點 |
缺點 |
互斥鎖 |
l 思路簡單 l 保持一致性 |
l 代碼復雜度大 l 存在死鎖風險 l 存在線程阻塞風險 |
永遠不過期 |
基本杜絕熱點key問題 |
l 不保證一致性 l 邏輯過期時間增加代碼維護成本和內存成本 |