深刻學習Redis之緩存設計與優化

緩存的使用與設計

緩存的收益與成本

收益node

  • 加速讀寫:CPU L1/L2/L3 Cache、瀏覽器緩存等。由於緩存一般都是全內存的(例如 Redis、Memcache),而 存儲層一般讀寫性能不夠強悍(例如 MySQL),經過緩存的使用能夠有效 地加速讀寫,優化用戶體驗。
  • 下降後端負載:幫助後端減小訪問量和複雜計算,在很大程度下降了後端的負載。

成本算法

  • 數據不一致:緩存層和數據層有時間窗口不一致,和更新策略有關。
  • 代碼維護成本:加入緩存後,須要同時處理緩存層和存儲層的邏輯, 增大了開發者維護代碼的成本。
  • 運維成本:以 Redis Cluster 爲例,加入後無形中增長了運維成本。

使用場景數據庫

  • 下降後端負載:對高消耗的 SQL:join 結果集/分組統計結果緩存。
  • 加速請求響應:利用 Redis/Memcache 優化 IO 響應時間。
  • 大量寫合併爲批量寫:好比計數器先 Redis 累加再批量寫入 DB。

緩存更新策略

緩存中的數據一般都是有生命週期的,須要在指定時間後被刪除或更新,這樣能夠保證緩存空間在一個可控的範圍。編程

可是緩存中的數據會和數據源中的真實數據有一段時間窗口的不一致,須要利用某些策略進行更新後端

下面將分別從使用場景、一致性、開發人員開發/維護成本三個方面介紹三種緩存的更新策略。瀏覽器

LRU/LFU/FIFO 算法剔除

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 查出的全部列呢,仍是某一些比較重要經常使用的列。

上述這個問題就是緩存粒度問題。

下面將從通用性空間佔用代碼維護三個角度進行說明:

  • 通用性:緩存所有數據比部分數據更加通用,但從實際經驗看,很長時間內應用只須要幾個重要的屬性。
  • 空間佔用:緩存所有數據要比部分數據佔用更多的空間,可能存在如下問題:

    • 所有數據會形成內存的浪費。
    • 所有數據可能每次傳輸產生的網絡流量會比較大,耗時相對較大,在極端狀況下會阻塞網絡。
    • 所有數據的序列化和反序列化的 CPU 開銷更大。
  • 代碼維護:所有數據的優點更加明顯,而部分數據一旦要加新字段須要修改業務代碼,並且修改後一般還須要刷新緩存數據。

緩存穿透問題

緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,一般出於容錯的考慮,若是從存儲層查不到數據則不寫入緩存層。分爲如下三步:

  1. 緩存層不命中。
  2. 存儲層不命中,不將空結果寫回緩存。
  3. 返回空結果。

緩存穿透帶來的問題

  • 緩存穿透將致使不存在的數據每次請求都要到存儲層去查詢,失去了緩存保護後端存儲的意義
  • 緩存穿透問題可能會使後端存儲負載加大,因爲不少後端存儲不具有高併發性,甚至可能形成後端存儲宕掉。一般能夠在程序中分別統計總調用數、緩存層命中數、存儲層命中數,若是發現大量存儲層空命中,可能就是出現了緩存穿透問題。

形成緩存穿透的緣由

  • 業務代碼自身問題。
  • 一些惡意攻擊、爬蟲等。

穿透優化的方案

  • 緩存空對象。
  • 布隆過濾器。

緩存空對象

其實也就是當第 2 步存儲層沒有命中後,仍然將空對象保留到緩存層中,以後再訪問這個數據將會從緩存中獲取。

這樣會帶來兩種問題:

  1. 空值作了緩存存儲。意味着緩存中須要更多的內存空間。因此咱們還須要針對這種空值增長一個過時時間,例如 1 分鐘,3 分鐘等等。具體仍是根據業務來判斷。
  2. 這樣作後會形成短時間內緩存層與存儲層有一段時間數據不一致問題,可能會對業務有所影響,好比咱們查詢商品 ID 爲 888,此時緩存層和存儲層都沒有此 ID 數據,咱們進行空值緩存後,若是此時剛好添加了 ID 爲 888 的數據,就會致使短時間內不一致問題。此時能夠利用消息系統或者其餘方式清除掉緩存層中的空對象

布隆過濾器

布隆過濾器是在訪問緩存層和存儲層以前,將存在的 key 用布隆過濾器提早保存起來,作第一層攔截

這種方法適用於數據命中不高、數據相對固定、實時性低(一般是數據集較大)的應用場景,代碼維護較爲複雜,可是緩存空間佔用少。

緩存雪崩問題

因爲 Cache 服務承載大量的請求,當 Cache 服務宕機後,大量的流量會直接壓向後端組件 DB,形成級聯故障。

優化方案

  1. 保證緩存高可用性,就算個別節點掛掉,依然還有別的能夠提供服務。
  2. 依賴隔離組件爲後端限流降級,好比使用 Hystrix。
  3. 提早演練。

無底洞問題

2010 年,Facebook 的 Memcache 節點已經達到了 3000 個,承載着 TB 級別的緩存數據。但開發和運維人員發現了一個問題,爲了知足業務要求添加了大量新 Memcache 節點,可是發現性能不但沒有好轉反而降低了,當時將這種現象稱爲緩存的「無底洞」現象。

那麼爲何會產生這種現象呢,一般來講添加節點使得 Memcache 集羣性能應該更強了,但事實並不是如此。鍵值數據庫因爲一般採用哈希函數將 key 映射到各個節點上,形成 key 的分佈與業務無關,可是因爲數據量和訪問量的持續增加,形成須要添加大量節點作水平擴容,致使鍵值分佈到更多的節點上,因此不管是 Memcache 仍是 Redis 的分佈式,批量操做一般須要從不一樣節點上獲取,相比於單機批量操做只涉及一次網絡操做,分佈式批量操做會涉及屢次網絡時間

優化思路

  • 命令自己的優化,例如:keys、hgetall、bigkey 等。
  • 減小網絡通訊次數。
  • 下降接入成本,例如客戶端使用長鏈接/鏈接池、NIO 等。

咱們下面重點如何下降網絡通訊次數

串行 mget

因爲 n 個 key 是比較均勻地分佈在 Redis Cluster 的各個節點上,所以沒法使用 mget 命令一次性獲取,因此一般來說要獲取 n 個 key 的值,最簡單的方法就是逐次執行 n 個 get 命令,這種操做時間複雜度較高,它的操做時間=n 次網絡時間+n 次命令時間。n 是 key 的數量,是最簡單的實現方式但顯然不是最優的。

串行 IO

Redis Cluster 使用 CRC16 算法計算出散列值,再取對 16383 的餘數就能夠算出 slot 值,有了這兩個數據就能夠將屬於同一個節點的 key 進行歸檔,獲得每一個節點的 key 子列表,以後對每一個節點執行 mget 或者 Pipeline 操做

它的操做時間=node 次網絡時間+n 次命令時間

這種方案比第一種要好一點,可是若是節點數太多,仍是有必定的性能問題

並行 IO

此方案是將方案 2 中的最後一步改成多線程執行,網絡次數雖然仍是節點個數,但因爲使用多線程網絡時間變爲 O(1),這種方案會增長編程的複雜度。操做時間爲max_slow(node 網絡時間)+n 次命令時間。

HASH_TAG

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)

熱點 key 的重建優化

咱們一般使用的「緩存+過時時間」的策略既能夠加速數據讀寫,又保證數據的按期更新,這種模式基本可以知足絕大部分需求。可是有兩個問題若是同時出現,可能就會對應用形成致命的危害:

  • 當前 key 是一個熱點 key(例如一個熱門的娛樂新聞),併發量很是大。
  • 重建緩存不能在短期完成,多是一個複雜計算

在緩存失效的瞬間,有大量線程來重建緩存,形成後端負載加大,甚至可能會讓應用崩潰。

咱們須要制定以下目標:

  • 減小重建緩存的次數。
  • 數據儘量一致。
  • 減小潛在危險。

下面咱們講解一下兩種解決方案。

互斥鎖

此方法只容許一個線程重建緩存,其餘線程等待重建緩存的線程執行完後,再從新從緩存獲取數據便可。

咱們可使用 Redis 的setnx命令來實現互斥鎖。

  1. 若是 Redis 數據存在則返回,不存在就進入第二步。
  2. 若是 setnx 結果爲 true,說明沒有其它線程重建,咱們執行重建緩存邏輯。
  3. 若是 setnx 結果爲 false,說明有其它的線程正在重建緩存,當前線程能夠睡眠指定時間後再去獲取緩存數據。

永遠不過時

緩存層面:沒有設置過時時間。

功能層面:爲每一個 value 設置一個邏輯過時時間,當發現超過邏輯過時時間後,會使用單獨的線程去構建緩存。

此方法能夠有效杜絕了熱點 key 產生的問題,但惟一不足的就是重構緩存期間,會出現數據不一致的狀況,這取決因而否能夠容忍這種不一致。

兩種方案對比

方案 優勢 缺點
互斥鎖 思路簡單,保證一致性 代碼複雜度增長,存在死鎖風險
永遠不過時 基本杜絕熱點 key 重建問題 不保證一致性,邏輯過時時間增長維護成本
相關文章
相關標籤/搜索