1. 程式人生 > >【快取】快取架構分析

【快取】快取架構分析

一、快取穿透預防及優化 

快取穿透是指查詢一個根本不存在的資料,快取層和儲存層都不會命中,但是出於容錯的考慮,如果從儲存層查不到資料則不寫入快取層,如圖 11-3 所示整個過程分為如下 3 步:

  1. 快取層不命中
  2. 儲存層不命中,所以不將空結果寫回快取
  3. 返回空結果 

    快取穿透將導致不存在的資料每次請求都要到儲存層去查詢,失去了快取保護後端儲存的意義。 
    這裡寫圖片描述 
        圖-1:快取穿透模型 
快取穿透問題可能會使後端儲存負載加大,由於很多後端儲存不具備高併發性,甚至可能造成後端儲存宕掉。通常可以在程式中分別統計總呼叫數、快取層命中數、儲存層命中數,如果發現大量儲存層空命中,可能就是出現了快取穿透問題。 
造成快取穿透的基本有兩個。第一,業務自身程式碼或者資料出現問題,第二,一些惡意攻擊、爬蟲等造成大量空命中,下面我們來看一下如何解決快取穿透問題。

二、快取穿透的解決方法

1)快取空物件

如下圖所示,當第 2 步儲存層不命中後,仍然將空物件保留到快取層中,之後再訪問這個資料將會從快取中獲取,保護了後端資料來源。 
這裡寫圖片描述 
快取空物件會有兩個問題: 
第一,空值做了快取,意味著快取層中存了更多的鍵,需要更多的記憶體空間 ( 如果是攻擊,問題更嚴重 ),比較有效的方法是針對這類資料設定一個較短的過期時間,讓其自動剔除。 
第二,快取層和儲存層的資料會有一段時間視窗的不一致,可能會對業務有一定影響。例如過期時間設定為 5 分鐘,如果此時儲存層添加了這個資料,那此段時間就會出現快取層和儲存層資料的不一致,此時可以利用訊息系統或者其他方式清除掉快取層中的空物件。 
下面給出了快取空物件的實現虛擬碼: 
這裡寫圖片描述

2)布隆過濾器攔截

如下圖所示,在訪問快取層和儲存層之前,將存在的 key 用布隆過濾器提前儲存起來,做第一層攔截。

例如: 一個個性化推薦系統有 4 億個使用者 ID,每個小時演算法工程師會根據每個使用者之前歷史行為做出來的個性化放到儲存層中,但是最新的使用者由於沒有歷史行為,就會發生快取穿透的行為,為此可以將所有有個性化推薦資料的使用者做成布隆過濾器。如果布隆過濾器認為該使用者 ID 不存在,那麼就不會訪問儲存層,在一定程度保護了儲存層。 
開發提示: 
有關布隆過濾器的相關知識,可以參考: Bloom Filter(布隆過濾器)的概念和原理

可以利用 Redis 的 Bitmaps 實現布隆過濾器,GitHub 上已經開源了類似的方案,讀者可以進行參考: 

https://github.com/erikdubbelboer/Redis-Lua-scaling-bloom-filter 
這裡寫圖片描述 
使用布隆過濾器應對穿透問題 
這種方法適用於資料命中不高,資料相對固定實時性低(通常是資料集較大)的應用場景,程式碼維護較為複雜,但是快取空間佔用少。 

兩種方案對比

前面介紹了快取穿透問題的兩種解決方法 ( 實際上這個問題是一個開放問題,有很多解決方法 ),下面通過下表從適用場景和維護成本兩個方面對兩種方案進行分析。 
快取空物件和布隆過濾器方案對比 
這裡寫圖片描述

三、快取雪崩問題優化 

從下圖可以很清晰出什麼是快取雪崩:由於快取層承載著大量請求,有效的保護了儲存層,但是如果快取層由於某些原因整體不能提供服務,於是所有的請求都會達到儲存層,儲存層的呼叫量會暴增,造成儲存層也會掛掉的情況。快取雪崩的英文原意是 stampeding herd(奔逃的野牛),指的是快取層宕掉後,流量會像奔逃的野牛一樣,打向後端儲存。 
這裡寫圖片描述 
快取層不可用引起的雪崩 
預防和解決快取雪崩問題,可以從以下三個方面進行著手。 
1)保證快取層服務高可用性。 
和飛機都有多個引擎一樣,如果快取層設計成高可用的,即使個別節點、個別機器、甚至是機房宕掉,依然可以提供服務,例如前面介紹過的 Redis Sentinel 和 Redis Cluster 都實現了高可用。 
2)依賴隔離元件為後端限流並降級。 
無論是快取層還是儲存層都會有出錯的概率,可以將它們視同為資源。作為併發量較大的系統,假如有一個資源不可用,可能會造成執行緒全部 hang 在這個資源上,造成整個系統不可用。降級在高併發系統中是非常正常的:比如推薦服務中,如果個性化推薦服務不可用,可以降級補充熱點資料,不至於造成前端頁面是開天窗。 
在實際專案中,我們需要對重要的資源 ( 例如 Redis、 MySQL、 Hbase、外部介面 ) 都進行隔離,讓每種資源都單獨執行在自己的執行緒池中,即使個別資源出現了問題,對其他服務沒有影響。但是執行緒池如何管理,比如如何關閉資源池,開啟資源池,資源池閥值管理,這些做起來還是相當複雜的,這裡推薦一個 Java 依賴隔離工具 Hystrix(https://github.com/Netflix/Hystrix),如下圖所示。 
Hystrix 是解決依賴隔離的利器,但是該內容已經超出本書的範圍,同時只適用於 Java 應用,所以這裡不會詳細介紹。 
這裡寫圖片描述 
Hystrix 示意圖 
3)提前演練。在專案上線前,演練快取層宕掉後,應用以及後端的負載情況以及可能出現的問題,在此基礎上做一些預案設定。 

四、快取熱點 key 重建優化 

開發人員使用快取 + 過期時間的策略既可以加速資料讀寫,又保證資料的定期更新,這種模式基本能夠滿足絕大部分需求。但是有兩個問題如果同時出現,可能就會對應用造成致命的危害:

  1. 當前 key 是一個熱點 key( 例如一個熱門的娛樂新聞),併發量非常大。
  2. 重建快取不能在短時間完成,可能是一個複雜計算,例如複雜的 SQL、多次 IO、多個依賴等。

在快取失效的瞬間,有大量執行緒來重建快取 ( 如下圖),造成後端負載加大,甚至可能會讓應用崩潰。 
這裡寫圖片描述 
熱點 key 失效後大量執行緒重建快取 
要解決這個問題也不是很複雜,但是不能為了解決這個問題給系統帶來更多的麻煩,所以需要制定如下目標:

    1. 減少重建快取的次數
    2. 資料儘可能一致
    3. 較少的潛在危險 
      1)互斥鎖 (mutex key)
      此方法只允許一個執行緒重建快取,其他執行緒等待重建快取的執行緒執行完,重新從快取獲取資料即可,整個過程如圖 : 
      這裡寫圖片描述
      使用互斥鎖重建快取 
      下面程式碼使用 Redis 的 setnx 命令實現上述功能。 
      這裡寫圖片描述
      (1) 從 Redis 獲取資料,如果值不為空,則直接返回值,否則執行 (2.1) 和 (2.2)。 
      (2) 如果 set(nx 和 ex) 結果為 true,說明此時沒有其他執行緒重建快取,那麼當前執行緒執行快取構建邏輯。 
      (2.2) 如果 setnx(nx 和 ex) 結果為 false,說明此時已經有其他執行緒正在執行構建快取的工作,那麼當前執行緒將休息指定時間 ( 例如這裡是 50 毫秒,取決於構建快取的速度 ) 後,重新執行函式,直到獲取到資料。 
      2)永遠不過期
      “永遠不過期”包含兩層意思: 
      從快取層面來看,確實沒有設定過期時間,所以不會出現熱點 key 過期後產生的問題,也就是“物理”不過期。 
      從功能層面來看,為每個 value 設定一個邏輯過期時間,當發現超過邏輯過期時間後,會使用單獨的執行緒去構建快取。 
      整個過程如下圖所示: 
      這裡寫圖片描述
      ” 永遠不過期 ” 策略 
      從實戰看,此方法有效杜絕了熱點 key 產生的問題,但唯一不足的就是重構快取期間,會出現資料不一致的情況,這取決於應用方是否容忍這種不一致。下面程式碼使用 Redis 進行模擬: 
      這裡寫圖片描述
      作為一個併發量較大的應用,在使用快取時有三個目標:第一,加快使用者訪問速度,提高使用者體驗。第二,降低後端負載,減少潛在的風險,保證系統平穩。第三,保證資料“儘可能”及時更新。下面將按照這三個維度對上述兩種解決方案進行分析。 
      互斥鎖 (mutex key):這種方案思路比較簡單,但是存在一定的隱患,如果構建快取過程出現問題或者時間較長,可能會存在死鎖和執行緒池阻塞的風險,但是這種方法能夠較好的降低後端儲存負載並在一致性上做的比較好。
      ” 永遠不過期 “:這種方案由於沒有設定真正的過期時間,實際上已經不存在熱點 key 產生的一系列危害,但是會存在資料不一致的情況,同時代碼複雜度會增大。
      兩種解決方法對比如下表所示。 
      兩種熱點 key 的解決方法 
      這裡寫圖片描述

轉載自微信:高可用架構