分布式緩存
說完進程內緩存,自然就過度到進程外緩存了。與進程內緩存不同,進程外緩存在應用運行的進程之外,它擁有更大的緩存容量,並且可以部署到不同的物理節點,通常會用分布式緩存的方式實現。
分布式緩存是與應用分離的緩存服務,最大的特點是,自身是一個獨立的應用/服務,與本地應用隔離,多個應用可直接共享一個或者多個緩存應用/服務。
分布式緩存簡圖
既然是分布式緩存,緩存的數據會分布到不同的緩存節點上,每個緩存節點緩存的數據大小通常也是有限制的。
數據被緩存到不同的節點,為了能方便的訪問這些節點,需要引入緩存代理,類似 Twemproxy。他會幫助請求找到對應的緩存節點。
同時如果緩存節點增加了,這個代理也會只能識別並且把新的緩存數據分片到新的節點,做橫向的擴展。
為了提高緩存的可用性,會在原有的緩存節點上加入 Master/Slave 的設計。當緩存數據寫入 Master 節點的時候,會同時同步一份到 Slave 節點。
一旦 Master 節點失效,可以通過代理直接切換到 Slave 節點,這時 Slave 節點就變成了 Master 節點,保證緩存的正常工作。
每個緩存節點還會提供緩存過期的機制,並且會把緩存內容定期以快照的方式保存到文件上,方便緩存崩潰之后啟動預熱加載。
高性能
當緩存做成分布式的時候,數據會根據一定的規律分配到每個緩存應用/服務上。
如果我們把這些緩存應用/服務叫做緩存節點,每個節點一般都可以緩存一定容量的數據,例如:Redis 一個節點可以緩存 2G 的數據。
如果需要緩存的數據量比較大就需要擴展多個緩存節點來實現,這么多的緩存節點,客戶端的請求不知道訪問哪個節點怎么辦?緩存的數據又如何放到這些節點上?
緩存代理服務已經幫我們解決這些問題了,例如:Twemproxy 不但可以幫助緩存路由,同時可以管理緩存節點。
這里有介紹三種緩存數據分片的算法,有了這些算法緩存代理就可以方便的找到分片的數據了。
①哈希算法
Hash 表是最常見的數據結構,實現方式是,對數據記錄的關鍵值進行 Hash,然后再對需要分片的緩存節點個數進行取模得到的余數進行數據分配。
例如:有三條記錄數據分別是 R1,R2,R3。他們的 ID 分別是 01,02,03,假設對這三個記錄的 ID 作為關鍵值進行 Hash 算法之后的結果依舊是 01,02,03。
我們想把這三條數據放到三個緩存節點中,可以把這個結果分別對 3 這個數字取模得到余數,這個余數就是這三條記錄分別放置的緩存節點。
Hash 算法是某種程度上的平均放置,策略比較簡單,如果要增加緩存節點,對已經存在的數據會有較大的變動。
②一致性哈希算法
一致性 Hash 是將數據按照特征值映射到一個首尾相接的 Hash 環上,同時也將緩存節點映射到這個環上。
如果要緩存數據,通過數據的關鍵值(Key)在環上找到自己存放的位置。這些數據按照自身的 ID 取 Hash 之后得到的值按照順序在環上排列。
如果這個時候要插入一條新的數據其 ID 是 115,那么就應該插入到如下圖的位置。
同理如果要增加一個緩存節點 N4 150,也可以放到如下圖的位置。
這種算法對於增加緩存數據,和緩存節點的開銷相對比較小。
③Range Based 算法
這種方式是按照關鍵值(例如 ID)將數據划分成不同的區間,每個緩存節點負責一個或者多個區間。跟一致性哈希有點像。
例如:存在三個緩存節點分別是 N1,N2,N3。他們用來存放數據的區間分別是,N1(0, 100], N2(100, 200], N3(300, 400]。
那么數據根據自己 ID 作為關鍵字做 Hash 以后的結果就會分別對應放到這幾個區域里面了。
可用性
根據事物的兩面性,在分布式緩存帶來高性能的同時,我們也需要重視它的可用性。那么哪些潛在的風險是我們需要防范的呢?
①緩存雪崩
當緩存失效,緩存過期被清除,緩存更新的時候。請求是無法命中緩存的,這個時候請求會直接回源到數據庫。
如果上述情況頻繁發生或者同時發生的時候,就會造成大面積的請求直接到數據庫,造成數據庫訪問瓶頸。我們稱這種情況為緩存雪崩。
從如下兩方面來思考解決方案:
-
緩存方面
-
設計方面
緩存方面:
-
避免緩存同時失效,不同的 key 設置不同的超時時間。
-
增加互斥鎖,對緩存的更新操作進行加鎖保護,保證只有一個線程進行緩存更新。緩存一旦失效可以通過緩存快照的方式迅速重建緩存。對緩存節點增加主備機制,當主緩存失效以后切換到備用緩存繼續工作。
設計方面,這里給出了幾點建議供大家參考:
-
熔斷機制:某個緩存節點不能工作的時候,需要通知緩存代理不要把請求路由到該節點,減少用戶等待和請求時長。
-
限流機制:在接入層和代理層可以做限流(之前的文章講到過),當緩存服務無法支持高並發的時候,前端可以把無法響應的請求放入到隊列或者丟棄。
-
隔離機制:緩存無法提供服務或者正在預熱重建的時候,把該請求放入隊列中,這樣該請求因為被隔離就不會被路由到其他的緩存節點。
如此就不會因為這個節點的問題影響到其他節點。當緩存重建以后,再從隊列中取出請求依次處理。
②緩存穿透
緩存一般是 Key,Value 方式存在,一個 Key 對應的 Value 不存在時,請求會回源到數據庫。
假如對應的 Value 一直不存在,則會頻繁的請求數據庫,對數據庫造成訪問壓力。如果有人利用這個漏洞攻擊,就麻煩了。
解決方法:如果一個 Key 對應的 Value 查詢返回為空,我們仍然把這個空結果緩存起來,如果這個值沒有變化下次查詢就不會請求數據庫了。
將所有可能存在的數據哈希到一個足夠大的 Bitmap 中,那么不存在的數據會被這個 Bitmap 過濾器攔截掉,避免對數據庫的查詢壓力。
③緩存擊穿
在數據請求的時候,某一個緩存剛好失效或者正在寫入緩存,同時這個緩存數據可能會在這個時間點被超高並發請求,成為“熱點”數據。
這就是緩存擊穿問題,這個和緩存雪崩的區別在於,這里是針對某一個緩存,前者是針對多個緩存。
解決方案:導致問題的原因是在同一時間讀/寫緩存,所以只有保證同一時間只有一個線程寫,寫完成以后,其他的請求再使用緩存就可以了。
比較常用的做法是使用 mutex(互斥鎖)。在緩存失效的時候,不是立即寫入緩存,而是先設置一個 mutex(互斥鎖)。當緩存被寫入完成以后,再放開這個鎖讓請求進行訪問。
總結
-
HTTP 緩存包括強制緩存和對比緩存。
-
CDN 緩存和 HTTP 緩存是好搭檔。
-
負載均衡器緩存相對穩定資源,需要服務協助工作。
-
進程內緩存,效率高,但容量有限制,有兩個方案可以應對緩存同步的問題。
-
分布式緩存容量大,能力強,牢記三個性能算法並且防范三個緩存風險。
注:本文摘自51CTO技術棧