高可用Redis(十三):Redis緩存的使用和設計

1.緩存的受益和成本

1.1 受益

1.能夠加速讀寫:Redis是基於內存的數據源,經過緩存加速數據讀取速度
2.下降後端負載:後端服務器經過前端緩存下降負載,業務端使用Redis下降後端數據源的負載等

1.2 成本

1.數據不一致:後端數據源中的數據緩存到Redis,若是後端數據庫中的數據被更新時,根據更新策略不一樣,Redis緩存層中的數據和數據源的數據有時間窗口不一致
2.代碼維護成本:多了一層緩存邏輯,之前只須要讀取後端數據庫,如今還須要維護緩存的讀寫以及Redis與數據庫的鏈接等
3.運維成本:例如Redis Cluster

1.3 使用場景

1.下降後端負載:對高消耗的SQL,例如作排行榜的計算涉及到不少張數據表上數據的很複雜的實時計算,這種計算實際上沒有任何意義,
    若是使用Redis緩存,只須要第一次把計算結果寫入到Redis緩存中,後續的計算直接在Redis中就能夠了,join結果集/分組統計結果進行緩存
2.加速請求響應:因爲Redis中的數據是保存在內存中的,利用Redis能夠顯著的提升IO響應時間
3.大量寫請求合併爲批量寫:如計數器先使用Redis進行累加,最後把結果批量寫入到後端數據庫中,而不用每次都更新到後端數據庫,有效下降後端數據庫的負載

2.緩存的更新策略

緩存中的數據有生命週期,須要按期更新和刪除,保證內存空間的合理使用以及緩存數據的一致,緩存數據須要根據合理的數據更新策略更新緩存中的數據前端

  • LRU/LFU/FIFO算法剔除:Redis使用maxmemory-policy,即Redis中的數據佔用的內存超過設定的最大內存時的操做策略
  • 超時剔除:對緩存的數據設置過時時間,超過過時時間自動刪除緩存數據,而後再次進行緩存,保證與數據庫中的數據一致
  • 主動更新:開發者控制key的更新週期,當key在後端數據庫中發生更新時,向Redis主動發送消息,Redis接收到消息對key進行更新或刪除

Redis的配置文件中定義了下面的緩存更新策略node

volatile-lru -> remove the key with an expire set using an LRU algorithm        # 根據LRU算法刪除過時的key 
allkeys-lru -> remove any key according to the LRU algorithm                    # 根據LRU算法刪除一些key
volatile-random -> remove a random key with an expire set                       # 隨機刪除一些設置了過時時間的key
allkeys-random -> remove a random key, any key                              # 從全部的key中隨機刪除一些key
volatile-ttl -> remove the key with the nearest expire time (minor TTL)     # 刪除一些快過時的key
noeviction -> don't expire at all, just return an error on write operations # 不刪除任何key,在向Redis寫入key時返回一個錯誤,這將會佔用更多的內存

須要注意的是:with any of the above policies, Redis will return an error on write operations, when there are no suitable keys for eviction。即在上面的六種策略中,若是沒有key能夠被刪除時,向Redis中寫入數據會返回一個error異常算法

LRU和最小TTL算法並非精確的算法,而是近似的算法(爲了節省內存)。所以能夠根據速度或準確性對其進行優化設置,使用maxmemory-samples選項來設置這個值數據庫

默認狀況下,maxmemory-samples的值設置爲5,即Redis將檢查5個鍵並選擇使用最少的一個key,若是設置爲10,很是接近真實的LRU算法,可是另外消耗一些的CPU。若是設置爲3則會加快Redis,但執行結果不夠準確。後端

緩存更新策略對比緩存

2.1 對於緩存的建議

  • 對數據一致性要求不高,即真實數據和緩存數據差異較大對業務影響不大狀況下,能夠採用最大內存和淘汰策略,內存使用量超過maxmemory-policy時,自動刪除數據,而不會影響業務
  • 對數據一致性要求較高,即真實數據和緩存數據差異較大會影響業務狀況下,能夠採用超時剔除和主動更新結合策略,由最大內存和淘汰策略兜底。若是主動更新的功能出現問題失效,沒有把一些沒必要要的數據刪除時,Redis佔用的內存會愈來愈多,此時能夠給一些有生命週期的key設置比較長的過時時間,而後設置maxmemorymaxmemory-policy,來保證Redis佔用的內存超過設置的最大內存時刪除一些過時的key,來保證Redis的高可用
  • 3.緩存粒度控制

上圖中,使用Redis來作緩存,底層使用MySQL來作數據存儲源,這種架構下大部分請求由Redis處理,少部分請求到達MySQL。服務器

從MySQL中獲取一個用戶的全部信息,而後緩存到Redis的數據結構中。網絡

此時須要面對一個問題:緩存這個用戶的全部數據信息,仍是緩存用戶須要的用戶信息字段。數據結構

能夠從三個角度來考慮:多線程

3.1 通用性

從通用性角度考慮,緩存全量屬性更好。

當用戶數據表字段發生改變時,不須要修改程序就能夠直接同步修改以後的用戶信息到Redis緩存中供用戶使用,可是用佔用更多的內存空間

3.2 佔用空間

從佔用空間的角度考慮,緩存部分屬性更好.

一樣當用戶數據表字段發生改變時而用戶須要這個字段信息時,就須要修改程序源代碼來把修改以後的用戶信息同步緩存到Redis中,這種狀況下佔用的內存空間比全量屬性佔用的內存空間要少

3.3 代碼維護

從代碼維護角度考慮,表面上全量屬性更好。

無論數據源中的數據表結構如何改變,都會把全部的數據同步到Redis緩存中,而不須要修改程序源代碼,可是在大多數狀況下,不會使用到全量數據,只須要緩存須要的數據就能夠了,從內存空間消耗及性能方面考慮,使用部分屬性更好

3.4 總結

選擇緩存屬性時,須要綜合考慮緩存全量屬性仍是部分屬性

4.緩存穿透優化

4.1 什麼叫緩存穿透

正常狀況下,客戶端從緩存中獲取數據,若是緩存中沒有用戶請求須要的數據,就會讀取數據源中的數據返回給客戶端,同時把數據回寫到緩存中。這樣當下次客戶端再請求這個數據時,就能夠直接從緩存中獲取數據而不須要通過數據庫了。

若是客戶端獲取一個數據源中沒有的key時,先從緩存中獲取,獲取結果爲null,而後到數據源中獲取,一樣獲取結果爲null,這樣全部的請求都會到達數據源,這就是緩存穿透的基本過程

緩存的存在就是爲了保護數據源,緩存穿透以後會對數據源形成巨大的負載和壓力,這就失去了緩存的意義。

4.2 緩存穿透的緣由

業務程序自身的問題:如沒法對緩存進行回寫等邏輯bug
惡意攻擊,爬蟲等

4.3 緩存穿透的發現

根據業務的響應時間來進行判斷,當業務的響應時間遠遠過正常狀況下的響應時間時,頗有可能就是緩存穿透形成的

能夠經過監控一些指標:總調用數,緩存層命中數,存儲層命中數等發現緩存穿透

4.4 緩存穿透解決辦法

4.4.1 緩存空對象

緩存空對象是一種簡單粗暴的解決方法

當數據源中沒有用戶請求須要的數據時,會請求數據源,以前的作法是數據源返回一個null,而緩存中並不作回寫,緩存空對象的作法就是把null回寫到緩存中,暫時解決緩存穿透帶來的壓力

緩存空對象會形成兩個問題

1.若是是惡意攻擊和爬蟲等,若是每次請求的數據都不一致,緩存空對象時會在緩存中設置不少的key,即便這些key的值都爲空值,也會佔用不少的內存空間,此時能夠爲這個key設置過時時間來下降這樣的風險

2.緩存空對象並設置過時時間,在這個時間內即便數據源恢復正常,請求獲得的結果仍然是null,形成緩存層和存儲層數據短時間不一致。這種狀況下,能夠經過訂閱發佈消息來解決,當數據源恢復正常時,會發布消息,而後把正常數據緩存到Redis中

4.4.2 布隆過濾器攔截

使用布隆過濾器能夠經過佔用很小的內存來對數據進行過濾

布隆過濾器攔截是把全部的key或者離散數據保存到布隆過濾器中,而後使用布隆過濾器在緩存層以前再作一層攔截。

若是請求沒有被布隆過濾器攔截,則會到達緩存層獲取須要的數據並返回,以達到實際效果

布隆過濾器對於固定的數據能夠起到很好的效果,可是對於頻繁更新的數據,布隆過濾器的構建會面臨不少問題

4.4.3 緩存穿透解決辦法對比

1.緩存空對象代碼層面比較簡單,可是須要一些額外的內存空間來保存空對象,並且會有短期內的數據不一致性
2.布隆過濾器須要特殊的使用場景,布隆過濾器須要維護一些單獨的代碼,並且布隆過濾器也會佔用額外的不多的內存空間來實現數據的過濾

5.無底洞問題優化

5.1 無底洞問題描述

2010年,Facebook已經有了3000個Memcache節點,Facebook發現問題:"加"Memcache節點,客戶端批量操做的效率不只沒有提高,反而降低,這就是一個無底洞問題

5.2 無底洞問題關鍵點

當只有一個節點時,執行一次mget只產生一次網絡IO;而當節點增長到3個時,使用順序IO方式執行一次mget就會產生三次網絡IO

同理,當節點愈來愈多,執行一次mget所須要的網絡時間也愈來愈多,會對客戶端的執行效率帶來很大的降低

實際上網絡IO因爲擴容已經由原來的O(1)變成O(node)了,節點越多,並行執行一次mget命令所須要的時間就越長,若是串行執行mget命令所須要的時間就更多了。

無底洞問題關鍵點即:

  • 更多的機器 != 更多的性能
  • 批量接口需求(mget和mset等):在執行mget和mset等命令時會面對的問題
  • 數據增加與水平擴展需求等:隨着業務量愈來愈大,對於緩存和數據源存儲的需求也是愈來愈大,就須要對緩存和數據源進行擴容,即增長緩存節點和數據源節點,可是節點數量增多並不能帶來性能的提高,這是一個矛盾的問題

    5.3 優化IO的方法

  • 優化命令自己:例如執行慢查詢keys,hgetall bigkey等命令時,儘可能選擇在緩存節點壓力不大時執行
  • 減小網絡通訊次數,例如執行mget命令由原來的O(n)次網絡時間縮減爲O(node)次網絡時間,
  • 下降接入成本:例如客戶端長鏈接/鏈接池,NIO等

    5.4 四種批量優化方法:

    5.4.1 串行mget

    串行mget須要n次網絡時間

5.4.2 串行IO

因爲客戶端對key進行從新組裝,因此把網絡通訊時間下降到節點次O(node)

5.4.3 並行IO

並行IO也會在客戶端對key進行從新組裝,而後執行並行操做,所須要的網絡時間爲O(1)

5.4.4 hash_tag

hash_tag會把全部的key都分配到一個節點,可是使用這種方法會遇到各類問題

5.5 四種優化方案的優缺點分析

6.執行key重建優化

6.1 緩存重建過程描述

在正常狀況下,客戶端發送請求,會先到緩存,從緩存中獲取須要的數據,若是緩存中並無須要數據,纔會繼續向數據源請求,從數據源中獲取數據返回給客戶端並回寫到緩存中,這就是緩存的重建過程

6.2 緩存重建問題描述

若是重建的是一個熱點key,用戶訪問量很是大。不少用戶發送請求獲取數據,執行線程從緩存中獲取數據,可是此時緩存中並有這些數據,就會從數據源中獲取數據,而後重建緩存。

當緩存重建完成,後續的訪問纔會直接讀取緩存數制並返回

在這個過程當中,會有不少線程同時查詢並重建緩存key,一方面會對數據源形成很大壓力,另外一方面也會加大響應的時間

6.3 解決緩存重建的目標

減小緩存重建次數:不要屢次重建緩存
數據儘量一致:緩存中的數據要儘量與數據源中的數據保持一致
減小潛在風險:可能形成死鎖或者線程池大量被夯住等狀況

6.4 緩存重建解決方法

6.4.1 互斥鎖(mutex key)

互斥鎖是一種比較直觀和簡單的解決思路

第一個用戶從緩存中獲取數據,此時緩存中並無用戶須要的數據,會從數據源中重建緩存,

用戶在從數據源查詢獲取數據和重建緩存的過程當中加上一把鎖,當重建緩存完成之後再把鎖解開,並返回

當第二個用戶也想從緩存中獲取數據時,若是第一個用戶重建緩存的過程尚未結束,即鎖尚未被解開時,就會等待,一樣後續訪問的用戶也通過這樣一個過程

當緩存重建完成,鎖被解開,全部的用戶請求都從緩存中獲取數據並輸出

互斥鎖解決了緩存大量重建的過程,可是在緩存重建的過程當中會有一個等待時間,大量線程被夯住,有可能形成死鎖的狀況

6.4.2 數據永不過時

在緩存層面,每個key都不設置過時時間(沒有設置expire)
在功能層面中,爲每一個value添加邏輯過時時間,一旦發現超過邏輯過時時間後,會使用單獨的線程去構建緩存

須要注意的是

數據永不過時是一個異步的過程,即便緩存重建失敗,也不會形成線程夯住的問題
數據永不過時基本杜絕了熱點key的重建問題。
數據永不過時好處是:相比於使用互斥鎖的方案,不會使用戶產生一個等待的時間,並且能夠保證只有一個線程來完成數據源的查詢和緩存的重建
數據永不過時的缺點:在緩存重建完成以前,用戶從緩存中獲得的原來的數據有可能與從數據源中的新數據不一致的狀況
數據永不過時中設置邏輯過時時間,會爲每個key設置過時時間,會增長維護成本,佔用更多的內存空間。

6.4 緩存重建解決方法對比

相關文章
相關標籤/搜索