收益:node
加速讀寫
:CPU L1/L2/L3 Cache、瀏覽器緩存等。由於緩存一般都是全內存的(例如 Redis、Memcache),而 存儲層一般讀寫性能不夠強悍(例如 MySQL),經過緩存的使用能夠有效 地加速讀寫,優化用戶體驗。下降後端負載
:幫助後端減小訪問量和複雜計算,在很大程度下降了後端的負載。成本:算法
數據不一致
:緩存層和數據層有時間窗口不一致,和更新策略有關。代碼維護成本
:加入緩存後,須要同時處理緩存層和存儲層的邏輯, 增大了開發者維護代碼的成本。運維成本
:以 Redis Cluster 爲例,加入後無形中增長了運維成本。使用場景:數據庫
下降後端負載
:對高消耗的 SQL:join 結果集/分組統計結果緩存。加速請求響應
:利用 Redis/Memcache 優化 IO 響應時間。大量寫合併爲批量寫
:好比計數器先 Redis 累加再批量寫入 DB。緩存中的數據一般都是有生命週期的,須要在指定時間後被刪除或更新,這樣能夠保證緩存空間在一個可控的範圍。編程
可是緩存中的數據會和數據源中的真實數據有一段時間窗口的不一致,須要利用某些策略進行更新。後端
下面將分別從使用場景、一致性、開發人員開發/維護成本三個方面介紹三種緩存的更新策略。瀏覽器
LRU:Least Recently Used,最近最少使用。緩存
LFU:Least Frequently Used,最不常用。網絡
FIFO:First In First Out,先進先出。多線程
使用場景:剔除算法一般用於緩存使用量超過了預設的最大值時候,如何對現有的數據進行剔除。例如 Redis 使用maxmemory-policy
這個配置做爲內存最大值後對於數據的剔除策略。架構
一致性:要清理哪些數據是由具體算法決定,開發人員只能決定使用哪一種算法,因此數據的一致性是最差的。
維護成本:算法不須要開發人員本身來實現,一般只須要配置最大maxmemory
和對應的策略
便可。
使用場景:超時剔除經過給緩存數據設置過時時間,讓其在過時時間後自動刪除,例如 Redis 提供的 expire 命令。若是業務能夠容忍一段時間內,緩存層數據和存儲層數據不一致,那麼能夠爲其設置過時時間。在數據過時後,再從真實數據源獲取數據,從新放到緩存並設置過時時間。
一致性:一段時間窗口內(取決於過時時間長短)存在一致性問題,即緩存數據和真實數據源的數據不一致。
維護成本:維護成本不是很高,只需設置 expire 過時時間便可,固然前提是應用方容許這段時間可能發生的數據不一致。
使用場景:應用方對於數據的一致性要求高,須要在真實數據更新後, 當即更新緩存數據。例如能夠利用消息系統或者其餘方式通知緩存更新。
一致性:一致性最高,但若是主動更新發生了問題,那麼這條數據極可能很長時間不會更新,因此建議結合超時剔除一塊兒使用效果會更好。
維護成本:維護成本會比較高,開發者須要本身來完成更新,並保證更新操做的正確性。
策略 | 一致性 | 維護成本 |
---|---|---|
LRU/LFU/FIFO | 最差 | 低 |
超時剔除 | 較差 | 低 |
主動更新 | 最好 | 高 |
建議:
通常經常使用的架構就是緩存層使用 Redis,存儲層使用 MySQL。
好比:咱們如今須要緩存用戶信息。
第一步:從 MySQL 查詢,獲得結果。
第二步:放入緩存中。
可是,咱們是緩存 MySQL 查出的全部列呢,仍是某一些比較重要經常使用的列。
上述這個問題就是緩存粒度問題。
下面將從通用性、空間佔用、代碼維護三個角度進行說明:
空間佔用:緩存所有數據要比部分數據佔用更多的空間,可能存在如下問題:
緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,一般出於容錯的考慮,若是從存儲層查不到數據則不寫入緩存層。分爲如下三步:
緩存穿透帶來的問題:
形成緩存穿透的緣由:
穿透優化的方案:
其實也就是當第 2 步存儲層沒有命中後,仍然將空對象保留到緩存層中,以後再訪問這個數據將會從緩存中獲取。
這樣會帶來兩種問題:
布隆過濾器是在訪問緩存層和存儲層以前,將存在的 key 用布隆過濾器提早保存起來,作第一層攔截。
這種方法適用於數據命中不高、數據相對固定、實時性低(一般是數據集較大)的應用場景,代碼維護較爲複雜,可是緩存空間佔用少。
因爲 Cache 服務承載大量的請求,當 Cache 服務宕機後,大量的流量會直接壓向後端組件 DB,形成級聯故障。
優化方案
2010 年,Facebook 的 Memcache 節點已經達到了 3000 個,承載着 TB 級別的緩存數據。但開發和運維人員發現了一個問題,爲了知足業務要求添加了大量新 Memcache 節點,可是發現性能不但沒有好轉反而降低了,當時將這種現象稱爲緩存的「無底洞」現象。
那麼爲何會產生這種現象呢,一般來講添加節點使得 Memcache 集羣性能應該更強了,但事實並不是如此。鍵值數據庫因爲一般採用哈希函數將 key 映射到各個節點上,形成 key 的分佈與業務無關,可是因爲數據量和訪問量的持續增加,形成須要添加大量節點作水平擴容,致使鍵值分佈到更多的節點上,因此不管是 Memcache 仍是 Redis 的分佈式,批量操做一般須要從不一樣節點上獲取,相比於單機批量操做只涉及一次網絡操做,分佈式批量操做會涉及屢次網絡時間。
咱們下面重點如何下降網絡通訊次數。
因爲 n 個 key 是比較均勻地分佈在 Redis Cluster 的各個節點上,所以沒法使用 mget 命令一次性獲取,因此一般來說要獲取 n 個 key 的值,最簡單的方法就是逐次執行 n 個 get 命令,這種操做時間複雜度較高,它的操做時間=n 次網絡時間+n 次命令時間。n 是 key 的數量,是最簡單的實現方式但顯然不是最優的。
Redis Cluster 使用 CRC16 算法計算出散列值,再取對 16383 的餘數就能夠算出 slot 值,有了這兩個數據就能夠將屬於同一個節點的 key 進行歸檔,獲得每一個節點的 key 子列表,以後對每一個節點執行 mget 或者 Pipeline 操做。
它的操做時間=node 次網絡時間+n 次命令時間。
這種方案比第一種要好一點,可是若是節點數太多,仍是有必定的性能問題。
此方案是將方案 2 中的最後一步改成多線程執行,網絡次數雖然仍是節點個數,但因爲使用多線程網絡時間變爲 O(1),這種方案會增長編程的複雜度。操做時間爲max_slow(node 網絡時間)+n 次命令時間。
Redis Cluster 的 hash_tag 功能能夠強制將多個 key 強制分配到 一個節點上,它的操做時間=1 次網絡時間+n 次命令時間。
方案 | 優勢 | 缺點 | 時間複雜度 |
---|---|---|---|
串行命令 | 簡單,若是 key 少的話,性能能夠接受 | 大量 key 的話延遲嚴重 | O(keys) |
串行 IO | 簡單,少許節點,性能知足要求 | 大量節點的話延遲嚴重 | O(nodes) |
並行 IO | 並行特性,延遲取決於最慢的節點 | 編程複雜,多線程定位複雜 | O(max_slow(nodes)) |
hash_tag | 性能高 | 讀寫增長 tag 維護成本,tag 分佈易發生數據傾斜 | O(1) |
咱們一般使用的「緩存+過時時間」的策略既能夠加速數據讀寫,又保證數據的按期更新,這種模式基本可以知足絕大部分需求。可是有兩個問題若是同時出現,可能就會對應用形成致命的危害:
在緩存失效的瞬間,有大量線程來重建緩存,形成後端負載加大,甚至可能會讓應用崩潰。
咱們須要制定以下目標:
下面咱們講解一下兩種解決方案。
此方法只容許一個線程重建緩存,其餘線程等待重建緩存的線程執行完後,再從新從緩存獲取數據便可。
咱們可使用 Redis 的setnx
命令來實現互斥鎖。
緩存層面:沒有設置過時時間。
功能層面:爲每一個 value 設置一個邏輯過時時間,當發現超過邏輯過時時間後,會使用單獨的線程去構建緩存。
此方法能夠有效杜絕了熱點 key 產生的問題,但惟一不足的就是重構緩存期間,會出現數據不一致的狀況,這取決因而否能夠容忍這種不一致。
方案 | 優勢 | 缺點 |
---|---|---|
互斥鎖 | 思路簡單,保證一致性 | 代碼複雜度增長,存在死鎖風險 |
永遠不過時 | 基本杜絕熱點 key 重建問題 | 不保證一致性,邏輯過時時間增長維護成本 |