C# .NET 中的緩存實現詳情

一、緩存的基本概念

緩存 。這是一個簡單但非常有效的概念,這個想法的核心是記錄過程數據,重用操作結果。當執行繁重的操作時,我們會將結果保存在我們的 緩存容器中 。下次我們需要該結果時,我們將從緩存容器中拉出它,而不是再次執行繁重的操作。

例如,要獲取一個人的頭像,您可能需要訪問數據庫。我們不會每次都執行那次旅行,而是將 Avatar 保存在緩存中,每次需要時從內存中提取它。

緩存非常適用於不經常更改的數據。或者甚至更好,永遠不會改變。不斷變化的數據,比如當前機器的時間不應該被緩存,否則你會得到錯誤的結果。

二、緩存

有 3 種類型的緩存:

  • In-Memory Cache: 用於在單個進程中實現緩存。當進程終止時,緩存也隨之終止。如果您在多臺服務器上運行相同的進程,您將為每臺服務器提供一個單獨的緩存。
  •  持久性進程內緩存: 是指在進程內存之外備份緩存。它可能在文件中,也可能在數據庫中。這比較困難,但如果您的進程重新啟動,緩存不會丟失。最適合在獲取緩存項的情況下使用范圍廣泛,並且您的進程往往會重新啟動很多。
  • 分佈式緩存: 是指您希望為多臺機器共享緩存。通常,它將是多個服務器。使用分佈式緩存,它存儲在外部服務中。這意味著如果一臺服務器保存瞭一個緩存項,其他服務器也可以使用它。像 Redis [1] 這樣的服務非常適合這一點。

我們將隻討論 進程內緩存

三、進程內緩存早期做法

讓我們用 C# 創建一個非常簡單的緩存實現:

public class NaiveCache<TItem>


{


    Dictionary<object, TItem> _cache = new Dictionary<object, TItem>();

 

 

    public TItem GetOrCreate(object key, Func<TItem> createItem)


    {


        if (!_cache.ContainsKey(key))


        {


            _cache[key] = createItem();


        }


        return _cache[key];


    }


}

用法:

var _avatarCache = new NaiveCache<byte[]>();


// ...


var myAvatar = _avatarCache.GetOrCreate(userId, () => _database.GetAvatar(userId));

這個簡單的代碼解決瞭一個關鍵問題。要獲取用戶的頭像,隻有第一個請求才會真正執行到數據庫的訪問。然後將頭像數據 ( byte[] ) 保存在進程內存中。對頭像的所有後續請求都將從內存中提取,從而節省時間和資源。

但是,正如編程中的大多數事情一樣,沒有什麼是那麼簡單的。由於多種原因,上述解決方案並不好。一方面,這個實現 不是線程安全的 。從多個線程使用時可能會發生異常。除此之外,緩存的項目將永遠留在內存中,這實際上非常糟糕。

這就是我們應該從緩存中刪除項目的原因:

  • 緩存會占用大量內存,最終導致內存不足異常和崩潰。
  • 高內存消耗會導致 GC 壓力 (又名內存壓力)。在這種狀態下,垃圾收集器的工作量超出其應有的水平,從而損害瞭性能。
  •  如果數據發生變化,可能需要刷新緩存。我們的緩存基礎設施應該支持這種能力。

為瞭處理這些問題,緩存框架具有 驅逐策略 (又名 移除策略 )。這些是根據某些邏輯從緩存中刪除項目的規則。常見的驅逐政策有:

  • 無論如何, 絕對過期 策略將在固定時間後從緩存中刪除項目。
  • 如果在固定的時間段內未 訪問 某個項目,則 滑動過期 策略將從緩存中刪除該項目。因此,如果我將過期時間設置為 1 分鐘,隻要我每 30 秒使用一次,該項目就會一直保留在緩存中。一旦我超過一分鐘不使用它,該物品就會被驅逐。
  • 大小限制 策略將限制緩存內存大小。

現在我們知道我們需要什麼,讓我們繼續尋找更好的解決方案。

四、更好的解決方案

作為一名博主,令我非常沮喪的是,微軟已經創建瞭一個很棒的緩存實現。這剝奪瞭我自己創建類似實現的樂趣,但至少我寫這篇博文的工作量減少瞭。

我將向您展示微軟的解決方案,如何有效地使用它,然後在某些場景中如何改進它。

System.Runtime.Caching/MemoryCache 與 Microsoft.Extensions.Caching.Memory

Microsoft 有 2 個解決方案 2 個不同的 NuGet 包用於緩存。兩者都很棒。根據 Microsoft 的 建議 ,更喜歡使用, Microsoft.Extensions.Caching.Memory 因為它與 Asp.NET Core 集成得更好。它可以很 容易地註入 到 Asp .NET Core 的依賴註入機制中。

1、 Microsoft.Extensions.Caching.Memory

這是一個基本示例 Microsoft.Extensions.Caching.Memory :

public class SimpleMemoryCache<TItem>


{

    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); 

    public TItem GetOrCreate(object key, Func<TItem> createItem)


    {


        TItem cacheEntry;


        if (!_cache.TryGetValue(key, out cacheEntry))// Look for cache key.


        {


            // Key not in cache, so get data.


            cacheEntry = createItem();


            // Save data in cache.


            _cache.Set(key, cacheEntry);


        }


        return cacheEntry;
    }
}

用法:

var _avatarCache = new SimpleMemoryCache<byte[]>();


// ...


var myAvatar = _avatarCache.GetOrCreate(userId, () => _database.GetAvatar(userId));

這和我自己的非常相似 NaiveCache ,所以有什麼改變?嗯,一方面,這是一個 線程安全的 實現。您可以一次從多個線程安全地調用它。

第二件事是 MemoryCache 允許我們之前談到的所有 驅逐政策 。

下面是一個例子:

2、具有驅逐策略的 IMemoryCache

public class MemoryCacheWithPolicy<TItem>
{
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions()

    {
        SizeLimit = 1024

    });

    public TItem GetOrCreate(object key, Func<TItem> createItem)

  {
        TItem cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry))// Look for cache key.

        {
            // Key not in cache, so get data.
            cacheEntry = createItem();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
             .SetSize(1)//Size amount
             //Priority on removing when reaching size limit (memory pressure)

                .SetPriority(CacheItemPriority.High)

            // Keep in cache for this time, reset time if accessed.

             .SetSlidingExpiration(TimeSpan.FromSeconds(2))

              // Remove from cache after this time, regardless of sliding expiration
                .SetAbsoluteExpiration(TimeSpan.FromSeconds(10));

            // Save data in cache.
            _cache.Set(key, cacheEntry, cacheEntryOptions);
        }
        return cacheEntry;
    }
}
  • SizeLimit 被添加到 MemoryCacheOptions . 這為我們的緩存容器添加瞭基於大小的策略。大小沒有單位。相反,我們需要在每個緩存條目上設置大小數量。在這種情況下,我們每次將金額設置為 1 SetSize(1) 。這意味著緩存限制為 1024 個項目。
  •   當我們達到大小限制時,應該刪除哪個緩存項?您實際上可以使用 .SetPriority(CacheItemPriority.High) . 級別為 Low、Normal、High 和 NeverRemove
  • SetSlidingExpiration(TimeSpan.FromSeconds(2)) 添加瞭,它將 滑動過期 時間設置為 2 秒。這意味著如果一個項目在 2 秒內未被訪問,它將被刪除。
  •   SetAbsoluteExpiration(TimeSpan.FromSeconds(10)) 添加瞭,將 絕對過期 時間設置為 10 秒。這意味著該項目將在 10 秒內被驅逐,如果它還沒有。
  • 除瞭示例中的選項之外,您還可以設置一個 RegisterPostEvictionCallback 委托,該委托將在項目被驅逐時調用。
  • 這是一個非常全面的功能集。它讓你想知道是否還有什麼要添加的。實際上有幾件事。

3、問題和缺失的功能

在這個實現中有幾個重要的缺失部分。

  • 雖然您可以設置大小限制,但緩存實際上並不監控 gc 壓力。如果真的監測,壓力大的時候可以收緊政策,壓力小的時候可以放松政策。
  • 當多個線程同時請求同一個項目時,請求不會等待第一個完成。該項目將被創建多次。例如,假設我們正在緩存頭像,從數據庫中獲取頭像需要 10 秒。如果我們在第一次請求後 2 秒請求頭像,它將檢查頭像是否已緩存(尚未緩存),並開始另一次訪問數據庫。

事實上,這是一個 MemoryCache 完全解決它的實現:

public class WaitToFinishMemoryCache<TItem>

{

    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
    private ConcurrentDictionary<object, SemaphoreSlim> _locks = new ConcurrentDictionary<object, SemaphoreSlim>();

    public async Task<TItem> GetOrCreate(object key, Func<Task<TItem>> createItem)

    {

        TItem cacheEntry;

        if (!_cache.TryGetValue(key, out cacheEntry))// Look for cache key.
        {
            SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));

            await mylock.WaitAsync();
            try
            {
                if (!_cache.TryGetValue(key, out cacheEntry))
                {
                    // Key not in cache, so get data.
                    cacheEntry = await createItem();

                    _cache.Set(key, cacheEntry);
                }
            }
            finally
            {
                mylock.Release();
            }

        }

        return cacheEntry;
    }

}

用法:

var _avatarCache = new WaitToFinishMemoryCache<byte[]>();
// ...
var myAvatar = 

 await _avatarCache.GetOrCreate(userId, async () => await _database.GetAvatar(userId));

4、代碼說明

此實現鎖定項目的創建。鎖是特定於鑰匙的。例如,如果我們正在等待獲取 Alex Avatar,我們仍然可以在另一個線程上獲取 John Sarah 的緩存值。

字典 _locks 存儲瞭所有的鎖。常規鎖不適用於 async/await ,因此我們需要使用 SemaphoreSlim [5] .

如果 (!_cache.TryGetValue(key, out cacheEntry)),有 2 次檢查以查看該值是否已被緩存。鎖內的那個是確保隻有一個創建的那個。鎖外面的那個是為瞭優化。

五、何時使用 WaitToFinishMemoryCache

這個實現顯然有一些開銷。讓我們考慮什麼時候甚至有必要。

在以下情況下使用 WaitToFinishMemoryCache:

  • 當項目的創建時間具有某種成本時,您希望盡可能減少創建。
  • 當一個項目的創建時間很長時。
  • 當必須確保每個鍵都創建一個項目時。

在以下情況下不要使用 WaitToFinishMemoryCache:

  • 沒有多個線程訪問同一個緩存項的危險。
  • 您不介意多次創建該項目。例如,如果對數據庫的額外訪問不會有太大變化。

概括:

緩存是一種非常強大的模式,它也很危險,並且有其自身的復雜性。緩存太多,可能會導致 GC 壓力,緩存太少會導致性能問題。而分佈式緩存,這是一個需要探索的全新世界。軟件開發職業就這樣,總是有新的東西要學習。

到此這篇關於C# .NET 中的緩存實現詳情的文章就介紹到這瞭,更多相關C# .NET 中的緩存實現內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: