邊緣緩存模式(Cache-Aside Pattern)


  邊緣緩存模式(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 緩存雪崩

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

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

參考資料

  《CloudDesignPatternsBook》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM