.NET中的緩存實現

軟件開發中最經常使用的模式之一是緩存,這是一個簡單但很是有效的概念,想法是重用操做結果,執行繁重的操做時,咱們會將結果保存在緩存容器中,下次咱們須要該結果時,咱們將從緩存容器中取出它,而不是再次執行繁重的操做。web

例如,要得到某人的頭像,您可能須要前往數據庫。咱們不會每次都執行那次查詢,而是將結果保存在緩存中,每次須要時都將其從內存中刪除。redis

緩存很是適合不常常更改的數據,甚至永遠不會改變。不斷變化的數據不適合緩存,如當前機器的時間不該緩存,不然您將獲得錯誤的結果。數據庫

進程內緩存,持久化緩存和分佈式緩存

  • 進程內緩存用於在單個進程中實現緩存時,當進程終止時,緩存會隨之消失。若是您在多個服務器上運行相同的進程,則每一個服務器都有一個單獨的緩存。
  • 持久化緩存是指在進程內存以外備份緩存,它可能位於文件中,也可能位於數據庫中。這實現比較困難,但若是從新啓動進程,緩存不會丟失。
  • 分佈式緩存是指您爲多臺計算機提供共享緩存,一般它將是幾個服務器,使用分佈式緩存,它存儲在外部服務中。這意味着若是一臺服務器保存了緩存項,其餘服務器也可使用它。Redis這樣的服務很是適合這種狀況。

單線程的緩存

public class NaiveCache<T>
{
    private static Dictionary<object, T> _cache = new Dictionary<object, T>();
    public static T GetOrCreate(object key, Func<T> createItem)
    {
        T cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry))
        {
            cacheEntry = createItem();
            _cache.Add(key, cacheEntry);
        }

        return cacheEntry;
    }
}

//用法
NaiveCache<string>.GetOrCreate("test", () => { return "test123"; });

這個簡單的代碼解決了一個關鍵問題,要獲取test的值,只有第一個請求才會實際執行數據庫操做,而後將數據保存在進程存儲器中,之後有關test的請求都將從內存中提取,從而節省時間和資源。編程

可是,做爲編程中的大多數事情,沒有什麼是如此簡單。因爲許多緣由,上述解決方案並很差。首先,這種實現不是線程安全的,多個線程使用時可能會發生異常,除此以外,緩存的項目將永遠留在內存中,這實際上很是糟糕。緩存

例如:安全

List<Task> t1 = new List<Task>();

foreach (var item in list)
{
    var a = Task.Run(() =>
    {
        Console.Write($"{NaiveCache<string>.GetOrCreate(item, () => { return item.ToString(); })}");
    });
    t1.Add(a);
}

try
{
    Task.WaitAll(t1.ToArray());
}
catch { }

運行結果7234859,運行 的數據丟失了服務器

這就是爲何咱們應該從Cache中刪除項目:

  1. 緩存可能佔用大量內存,最終致使內存不足異常和崩潰。
  2. 高內存消耗可致使GC壓力(又稱內存壓力)。在這種狀態下,垃圾收集器的工做量超出預期,會影響性能。
  3. 若是數據發生更改,可能須要刷新緩存,咱們的緩存基礎架構應該支持這種能力。

爲了處理這些問題,緩存框架具備驅逐策略(即刪除策略),這些是根據某些邏輯從緩存中刪除項目的規則,常見的驅逐政策是:架構

  • 絕對過時策略將在一段固定的時間後從緩存中刪除一個項目。
  • 若是未在固定的時間內訪問項目,則滑動過時策略將從緩存中刪除項目所以,若是我將到期時間設置爲1分鐘,只要我每隔30秒使用一次,該項目就會保持在緩存中,一旦我不使用它超過一分鐘,該項目被驅逐。
  • 大小限制策略將限制高速緩存大小。

如今咱們知道了咱們須要什麼,讓咱們繼續尋找更好的解決方案。框架

改善方案

令我很是沮喪的是,做爲博主,微軟已經建立了一個很棒的緩存實現,這剝奪了我本身建立相似實現的樂趣,但至少我寫這篇博文的工做較少。分佈式

我將向您展現Microsoft的解決方案,如何有效地使用它,以及如何在某些狀況下改進它。

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

微軟有2個解決方案,2個不一樣的NuGet包用於緩存,二者都很棒,根據微軟的建議,更喜歡使用Microsoft.Extensions.Caching.Memory由於它與Asp更好地集成.NET核心。它能夠很容易地注入到Asp .NET Core的依賴注入機制中。

這是一個基本的例子Microsoft.Extensions.Caching.Memory

/// <summary>
/// 利用微軟的庫寫的緩存
/// </summary>
/// <typeparam name="T"></typeparam>
public class SimpleMemoyCache<T>
{
    private static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

    public static T GetOrCreate(object key, Func<T> createItem) {
        T cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry)) {
            cacheEntry = createItem();
            _cache.Set(key, cacheEntry);
        }

        return cacheEntry;
    }
}

用法:

SimpleMemoyCache<string>.GetOrCreate("test", () => { return "test123"; });

這與我本身很是類似NaiveCache,因此改變了什麼?嗯,首先,這是一個線程安全的實現。您能夠安全地從多個線程一次調用它。

帶有逐出政策的IMemoryCache:

/// <summary>
/// 帶有策略的緩存
/// </summary>
/// <typeparam name="T"></typeparam>
public class MemoryCacheWithPolicy<T>
{
    /// <summary>
    /// 增長設置緩存大小
    /// </summary>
    private static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() { SizeLimit = 1024 });

    public static T GetOrCreate(object key, Func<T> createItem) {
        T cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry)) {
            cacheEntry = createItem();
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSize(1)
                .SetPriority(CacheItemPriority.High) //設置優先級
                .SetSlidingExpiration(TimeSpan.FromSeconds(2)) //2s沒有訪問刪除
                .SetAbsoluteExpiration(TimeSpan.FromSeconds(10)); //10s過時

            _cache.Set(key, cacheEntry, cacheEntryOptions);
        }

        return cacheEntry;
    }
}

讓咱們分析一下新增內容:

  1. SizeLimit加入了MemoryCacheOptions,這會將基於大小的策略添加到緩存容器中。相反,咱們須要在每一個緩存條目上設置大小,在這種狀況下,咱們每次設置爲1 SetSize(1),這意味着緩存限制爲1024個項目。
  2. 當咱們達到大小限制時,應該刪除哪一個緩存項?您實際上能夠設置優先級.SetPriority(CacheItemPriority.High)級別爲Low,Normal,HighNeverRemove
  3. SetSlidingExpiration(TimeSpan.FromSeconds(2))添加了,將滑動到期時間設置爲2秒,這意味着若是超過2秒內未訪問某個項目,它將被刪除。
  4. SetAbsoluteExpiration(TimeSpan.FromSeconds(10))添加了,它將絕對到期時間設置爲10秒,這意味着若是物品還沒有在10秒內被驅逐。

除了示例中的選項以外,您還能夠設置一個RegisterPostEvictionCallback委託,當項目被驅逐時將調用委託。

這是一個很是全面的功能集。它讓你想知道是否還有其餘東西要添加,實際上有幾件事。

問題和缺失的功能

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

  1. 雖然您能夠設置大小限制,但緩存實際上並不監視gc壓力。若是咱們確實對其進行監控,咱們能夠在壓力較大時收緊政策,並在壓力較低時放鬆政策。
  2. 當同時請求具備多個線程的相同項時,請求不等待第一個完成,該項目將被屢次建立。例如,假設咱們正在緩存阿凡達,從數據庫中獲取頭像須要10秒鐘,若是咱們在第一次請求後2秒請求頭像,它將檢查頭像是否被緩存(它尚未),並開始另外一次訪問數據庫。

英文原文中有說明,可是以爲不太好,再次沒有翻譯。

英文原文地址:

https://michaelscodingspot.com/cache-implementations-in-csharp-net/?utm_source=csharpdigest&utm_medium=web&utm_campaign=featured

代碼與所寫有所修改,可是大體意思同樣,若是感興趣,能夠看看英文。

相關文章
相關標籤/搜索