邊緣快取模式(Cache-Aside Pattern)

  • 2019 年 10 月 3 日
  • 筆記

  邊緣快取模式(Cache-Aside Pattern),即按需將數據從數據存儲載入到快取中。此模式最大的作用就是提高性能減少不必要的查詢。

1 模式

  1. 先從快取查詢數據
  2. 如果沒有命中快取則從數據存儲查詢
  3. 將數據寫入快取
  程式碼形如:
        public async Task<MyEntity> GetMyEntityAsync(int id)          {              // Define a unique key for this method and its parameters.                var key = string.Format("StoreWithCache_GetAsync_{0}", id);              var expiration = TimeSpan.FromMinutes(3);              bool cacheException = false;              try              {                  // Try to get the entity from the cache.                      var cacheItem = cache.GetCacheItem(key);                  if (cacheItem != null)                  {                      return cacheItem.Value as MyEntity;                  }              }              catch (DataCacheException)              {                  // If there is a cache related issue, raise an exception                  // and avoid using the cache for the rest of the call.                      cacheException = true;              }              // If there is a cache miss, get the entity from the original store and cache it.              // Code has been omitted because it is data store dependent.                  var entity = ...;              if (!cacheException)              {                  try                  {                      // Avoid caching a null value.                            if (entity != null)                      {                          // Put the item in the cache with a custom expiration time that                          // depends on how critical it might be to have stale data.                                  cache.Put(key, entity, timeout: expiration);                      }                  }                  catch (DataCacheException)                  {                      // If there is a cache related issue, ignore it                      // and just return the entity.                      }              }              return entity;          }            public async Task UpdateEntityAsync(MyEntity entity)          {              // Update the object in the original data store                await this.store.UpdateEntityAsync(entity).ConfigureAwait(false);              // Get the correct key for the cached object.                var key = this.GetAsyncCacheKey(entity.Id);              // Then, invalidate the current cache object                this.cache.Remove(key);          }            private string GetAsyncCacheKey(int objectId)          {              return string.Format("StoreWithCache_GetAsync_{0}", objectId);          }

2 關注點

2.1 快取數據的選擇

  對於相對靜態的數據或頻繁讀取的數據,快取是最有效的。

2.2 快取數據的生命周期

  過期時間短,會導致頻繁查詢數據源而失去快取的意義;設置時間過長,可能發生快取的數據過時不同步的情況。

2.3 快取過期策略的選擇

  一般快取產品都有自己內置的快取過期策略,最長使用的是“最近最不常使用”演算法,需要在使用時,了解產品的默認配置和可配置項,根據實際需求選擇。

2.4 本地快取與分散式快取的選擇

  本地快取的查詢速度更快,但在一個分散式環境中,需要保證不同機器的本地快取統一,這需要我們在程式中處理;分散式快取性能不如本地快取,但大多數時候,分散式快取更適合大型項目,分散式快取產品家族自帶的管理和診斷工具十分適合運維和性能監控。

2.5 一致性問題

  實現邊緣快取模式不能保證數據存儲和快取之間的實時一致性。數據存儲中的某個項可能隨時被外部進程更改,並且此更改可能在下次將項載入到快取中之前不會反映在快取中。

3 一致性

3.1 淘汰還是更新快取

淘汰快取:數據寫入數據存儲,並從快取刪除
優點:簡單
缺點:快取增加一次miss,需要重新載入數據到快取
  更新快取:數據寫入數據存儲,並更新快取
  優點:快取不會增加一次miss,命中率高
  缺點:更新快取比淘汰快取複雜
  淘汰還是更新快取主要取決於——更新快取的複雜度,數據查詢時間以及更新頻率。
  如果更新快取複雜度低,從數據存儲查詢查詢數據有比較耗時,更新頻率又高,則為了保證快取命中率,更新快取比較適合。此外大大多數情景,都可以用淘汰快取,來滿足需求。

3.2 操作順序

  先操作數據存儲,再操作快取
  先操作快取,再操作數據存儲

3.3 一致性問題

非並發場景——場景為Application修改數據(考慮操作失敗的影響)

先操作快取,再操作數據存儲

淘汰快取時:
step 1:淘汰快取成功
step 2:更新數據存儲失敗
結果:數據存儲未修改,快取已失效
修改快取時:
step 1:修改快取成功
step 2:更新數據存儲失敗
結果:數據不一致
先操作數據存儲,再操作快取
  淘汰快取時:
step 1:更新數據存儲成功
step 2:淘汰快取失敗
結果:數據不一致
  修改快取時:
step 1:更新數據存儲成功
step 2:修改快取失敗
結果:數據不一致

並發場景——場景位Application1修改數據,Application2讀取數據,(不考慮操作失敗的影響,僅考慮執行順序的影響)

先操作快取,再操作數據存儲
  淘汰快取時:
step 1:Application1先淘汰快取
step 2:Application2從快取讀取數據,但是沒有命中快取
step 3:Application2從數據存儲讀取舊數據
step 4:Application1完成數據存儲的更新
step 5:Application2把舊數據寫入快取,造成快取與數據存儲不一致
結果:數據不一致
  更新快取時:
step 1:Application1先更新快取
step 2:Application2從快取讀取到新數據
step 3:Application2
step 4:Application1更新數據存儲成功
step 5:Application2
結果:數據一致
先操作數據存儲,再操作存儲
  
  淘汰快取時:
step 1:Application1更新數據存儲完成
step 2:Application2從快取讀取數據,查詢到舊數據
step 3:Application1從快取中刪除數據
結果:快取已淘汰,下次載入新數據
  更新快取時:
step 1:Application1更新數據存儲完成
step 2:Application2從快取讀取數據,查詢到舊數據
step 3:Application1從更新快取數據
結果:數據一致
  由此可見,不管我們如何組織,都可能完全杜絕不一致問題,而影響一致性的兩個關鍵因素是——“操作失敗”和“時序”。
  對於“淘汰還是更新快取”、“先快取還是先數據存儲”的選擇,不應該脫離具體需求和業務場景而一概而論。我們把關注點重新回到“操作失敗”和“時序”問題上來.
  對於“操作失敗”,首先要從程式層面提高穩定性,比如“彈性編程”,“防禦式編程”等技巧,其次,要設計補償機制,在操作失敗後要做到保存足夠的資訊(包括補償操作需要的數據,異常資訊等),並進行補償操作(清洗異常數據,回滾數據到操作前的狀態),通過補償操作,實現最終一致性。
  對於“時序”問題,需要根據程式結構,梳理出潛在的時序問題。本文例子中,不設計補償操作,如果引入的話,操作組合的時序圖可能會更加複雜。解決的思路就是對於單個用戶來說操作應該是“原子的”在分散式環境中,多個用戶的操作應該是“串列”的。最簡單的思路,就是使用分散式鎖,來保證操作的串列化。當然,我們也可以通過隊列來進行非同步落庫,實現最終一致性。

4 快取穿透、快取擊穿、快取雪崩

4.1 快取穿透

  快取穿透是指用戶頻繁查詢數據存儲中不存在的數據,這類數據,查不到數據所以也不會寫入快取,所以每次都會查詢數據存儲,導致數據存儲壓力過大。
  解決方案:
    • 增加數據校驗
    • 查詢不到時,快取空對象

4.2 快取擊穿

  高並發下,當某個快取失效時,可能出現多個進程同時查詢數據存儲,導致數據存儲壓力過大。
  解決方案:
      • 使用二級快取
      • 通過加鎖或者隊列降低查詢資料庫存儲的並發數量
      • 考慮延長部分數據是過期時間,或者設置為永不過期

4.3 快取雪崩

  高並發下,大量快取同時失效,導致大量請求同時查詢數據存儲,導致數據存儲壓力過大。
  解決方案:
      • 使用二級快取
      • 通過加鎖或者隊列降低查詢資料庫存儲的並發數量
      • 根據數據的變化頻率,設置不同的過期時間,避免在同一時間大量失效
      • 考慮延長部分數據是過期時間,或者設置為永不過期

  總之,設計不能脫離具體需求和業務場景而存在,這裡沒有最優的組合方式,以上對該模式涉及問題的討論,旨在發掘潛在的問題,以便合理應對。