【Redis—進階】緩存設計

緩存的收益和成本

收益

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

成本

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

緩存的使用場景基本包含以下兩種:node

  • 開銷大的複雜計算:以MySQL爲例,一些複雜的操做或者計算(例如大量聯表操做、一些分組計算),若是不加緩存,不但沒法知足高併發量,同時也會給MySQL帶來巨大的負擔。
  • 加速請求響應:即便查詢單條後端數據足夠快,那麼依然可使用緩存,以Redis爲例,每秒能夠完成數萬次讀寫,而且提供的批量操做能夠優化整個IO鏈的響應時間。

緩存更新策略

LRU/LFU/FIFO算法剔除

使用場景:一般用於緩存使用量超過了預設的最大值時候,如何對現有的數據進行剔除。例如Redis使用maxmemory-policy這個配置做爲內存最大值後對於數據的剔除策略。算法

一致性:要清理哪些數據是由具體算法決定,開發人員只能決定使用哪一種算法,因此數據的一致性是最差的。數據庫

維護成本:算法不須要開發人員本身來實現,一般只須要配置最大maxmemory和對應的策略便可。開發人員只須要知道每種算法的含義,選擇適合本身的算法便可。編程

超時剔除

使用場景:超時剔除經過給緩存數據設置過時時間,讓其在過時時間後自動刪除,例如Redis提供的expire命令。若是業務能夠容忍一段時間內,緩存層數據和存儲層數據不一致,那麼能夠爲其設置過時時間。在數據過時後,再從真實數據源獲取數據,從新放到緩存並設置過時時間。後端

一致性:一段時間窗口內存在一致性問題,即緩存數據非和真實數據源的數據不一致。緩存

維護成本:維護成本不是很高,只需設置expire過時時間便可,固然前提是應用方容許這段時間可能發生的數據不一致。網絡

主動更新

使用場景:應用方對於數據的一致性要求高,須要在真實數據更新後,當即更新緩存數據。例如能夠利用消息系統或者其餘方式通知緩存更新。多線程

一致性:一致性最高,但若是主動更新發生了問題,那麼這條數據極可能很長時間不會更新,因此建議結合超時剔除一塊兒使用效果會更好。併發

維護成本:維護成本會比較高,開發者須要本身來完成更新,並保證更新操做的正確性。運維

最佳實踐

低一致性業務建議配置最大內存和淘汰策略的方式使用。高一致性業務能夠結合使用超時剔除和主動更新,這樣即便主動更新出了問題,也能保證數據過時時間後刪除髒數據。

緩存粒度控制

例如如今須要將MySQL的用戶信息使用Redis緩存,假設用戶表有100個列,須要緩存到什麼維度呢?這個問題就是緩存粒度問題,到底是緩存所有屬性仍是隻緩存部分重要屬性?下面將從通用性、空間佔用、代碼維護三個角度進行說明。

通用性:緩存所有數據比部分數據更加通用,但從實際經驗看,很長時間內應用只須要幾個重要的屬性。

空間佔用:緩存所有數據要比部分數據佔用更多的空間,可能存在如下問題:

  • 所有數據會形成內存的浪費。
  • 所有數據可能每次傳輸產生的網絡流量會比較大,耗時相對較大,在極端狀況下會阻塞網絡。
  • 所有數據的序列化和反序列化的CPU開銷更大。

代碼維護:所有數據的優點更加明顯,而部分數據一旦要加新字段須要修改業務代碼,並且修改後一般還須要刷新緩存數據。

緩存粒度問題是一個容易被忽視的問題,若是使用不當,可能會形成不少無用空間的浪費,網絡帶寬的浪費,代碼通用性較差等狀況,須要綜合數據通用性、空間佔用比、代碼維護性三點進行取捨。

緩存穿透

緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,一般出於容錯的考慮,若是從存儲層查不到數據則不寫入緩存層。緩存穿透將致使不存在的數據每次請求都要到存儲層去查詢,失去了緩存保護後端存儲的意義。

緩存穿透問題可能會使後端存儲負載加大,因爲不少後端存儲不具有高併發性,甚至可能形成後端存儲宕掉。一般能夠在程序中分別統計總調用數、緩存層命中數、存儲層命中數,若是發現大量存儲層空命中,可能就是出現了緩存穿透問題。

形成緩存穿透的基本緣由有兩個。第一,自身業務代碼或者數據出現問題,第二,一些惡意攻擊、爬蟲等形成大量空命中。下面咱們來看一下如何解決緩存穿透問題。

緩存空對象

當存儲層不命中後,仍然將空對象保留到緩存層中,以後再訪問這個數據將會從緩存中獲取,這樣就保護了後端數據源。

緩存空對象會有兩個問題:

  1. 空值作了緩存,意味着緩存層中存了更多的鍵,須要更多的內存空間(若是是攻擊,問題更嚴重),比較有效的方法是針對這類數據設置一個較短的過時時間,讓其自動剔除。
  2. 緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有必定影響。例如過時時間設置爲5分鐘,若是此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時能夠利用消息系統或者其餘方式清除掉緩存層中的空對象。
布隆過濾器攔截

在訪問緩存層和存儲層以前,將存在的key用布隆過濾器提早保存起來,作第一層攔截。例如:一個推薦系統有4億個用戶id,每一個小時算法工程師會根據每一個用戶以前歷史行爲計算出推薦數據放到存儲層中,可是最新的用戶因爲沒有歷史行爲,就會發生緩存穿透的行爲,爲此能夠將全部推薦數據的用戶作成布隆過濾器。若是布隆過濾器認爲該用戶id不存在,那麼就不會訪問存儲層,在必定程度保護了存儲層。

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

緩存空對象和布隆過濾器方案對比

image.png

緩存無底洞

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

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

無底洞問題分析:

  • 客戶端一次批量操做會涉及屢次網絡操做,也就意味着批量操做會隨着節點的增多,耗時會不斷增大。
  • 網絡鏈接數變多,對節點的性能也有必定影響。

用一句通俗的話總結就是,更多的節點不表明更高的性能,所謂「無底洞」就是說投入越多不必定產出越多。可是分佈式又是不能夠避免的,由於訪問量和數據量愈來愈大,一個節點根本抗不住,因此如何高效地在分佈式緩存中批量操做是一個難點。

下面介紹如何在分佈式條件下優化批量操做。在介紹具體的方法以前,咱們來看一下常見的單機IO優化思路:

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

這裏咱們假設命令、客戶端鏈接已經爲最優,重點討論減小網絡操做次數。以Redis批量獲取n個字符串爲例,咱們將結合Redis Cluster的一些特性對四種分佈式的批量操做方式進行說明。

串行命令

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

串行IO

Redis Cluster使用CRC16算法計算出散列值,再取對16383的餘數就能夠算出slot值,同時Smart客戶端會保存slot和節點的對應關係,有了這兩個數據就能夠將屬於同一個節點的key進行歸檔,獲得每一個節點的key子列表,以後對每一個節點執行mget或者Pipeline操做,它的操做時間=node次網絡時間+n次命令時間,網絡次數是node的個數,很明顯這種方案比第一種要好不少,可是若是節點數太多,仍是有必定的性能問題。

並行IO

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

hash_tag實現

使用Redis Cluster的hash_tag功能,它能夠將多個key強制分配到一個節點上,它的操做時間=1次網絡時間+n次命令時間。

方案對比

image.png

緩存雪崩

因爲緩存層承載着大量請求,有效地保護了存儲層,可是若是緩存層因爲某些緣由不能提供服務,因而全部的請求都會達到存儲層,存儲層的調用量會暴增,形成存儲層也會級聯宕機的狀況。

預防和解決緩存雪崩問題,能夠從如下三個方面進行着手。

保證緩存層服務高可用性

若是緩存層設計成高可用的,即便個別節點、個別機器、甚至是機房宕掉,依然能夠提供服務,例如Redis Sentinel和Redis Cluster都實現了高可用。

依賴隔離組件爲後端限流並降級

不管是緩存層仍是存儲層都會有出錯的機率,能夠將它們視同爲資源。做爲併發量較大的系統,假若有一個資源不可用,可能會形成線程所有阻塞在這個資源上,形成整個系統不可用。

降級機制在高併發系統中是很是廣泛的:好比推薦服務中,若是個性化推薦服務不可用,能夠降級補充熱點數據。在實際項目中,咱們須要對重要的資源(例如Redis、MySQL、HBase、外部接口)都進行隔離,讓每種資源都單獨運行在本身的線程池中,即便個別資源出現了問題,對其餘服務沒有影響。可是線程池如何管理,好比如何關閉資源池、開啓資源池、資源池閥值管理,這些作起來仍是至關複雜的。

提早演練

在項目上線前,演練緩存層宕掉後,應用以及後端的負載狀況以及可能出現的問題,在此基礎上作一些預案設定。

熱點key重建

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

  • 當前key是一個熱點key,併發量很是大。
  • 重建緩存不能在短期完成,多是一個複雜計算,例如複雜的SQL、屢次IO、多個依賴等。

在緩存失效的瞬間,有大量線程來重建緩存,形成後端負載加大,甚至可能會讓應用崩潰。要解決這個問題也不是很複雜,可是不能爲了解決這個問題給系統帶來更多的麻煩,因此須要制定以下目標:

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

此方法只容許一個線程重建緩存,其餘線程等待重建緩存的線程執行完,從新從緩存獲取數據便可。例如可使用Redis的setnx命令來實現一個簡單的分佈式互斥鎖來完成。

永遠不過時

「永遠不過時」包含兩層意思:

  • 從緩存層面來看,確實沒有設置過時時間,因此不會出現熱點key過時後產生的問題,也就是「物理」不過時。
  • 從功能層面來看,爲每一個value設置一個邏輯過時時間,當發現超過邏輯過時時間後,會使用單獨的線程去構建緩存

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

總結

做爲一個併發量較大的應用,在使用緩存時有三個目標:第一,加快用戶訪問速度,提升用戶體驗。第二,下降後端負載,減小潛在的風險,保證系統平穩。第三,保證數據「儘量」及時更新。下表是按照這三個維度對上述兩種解決方案所進行的對比。

image.png

相關文章
相關標籤/搜索