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 邏輯過期時間增加程式碼維護成本和記憶體成本 |