軟體開發中最常用的模式之一是緩存,這是一個簡單但非常有效的概念,想法是重用操作結果,執行繁重的操作時,我們會將結果保存在緩存容器中,下次我們需要該結果時,我們將從緩存容器中取出它,而不是再次執行繁重的操作。 例如,要獲得某人的頭像,您可能需要前往資料庫。我們不會每次都執行那次查詢,而是將結果保存在緩 ...
軟體開發中最常用的模式之一是緩存,這是一個簡單但非常有效的概念,想法是重用操作結果,執行繁重的操作時,我們會將結果保存在緩存容器中,下次我們需要該結果時,我們將從緩存容器中取出它,而不是再次執行繁重的操作。
例如,要獲得某人的頭像,您可能需要前往資料庫。我們不會每次都執行那次查詢,而是將結果保存在緩存中,每次需要時都將其從記憶體中刪除。
緩存非常適合不經常更改的數據,甚至永遠不會改變。不斷變化的數據不適合緩存,如當前機器的時間不應緩存,否則您將得到錯誤的結果。
進程內緩存,持久化緩存和分散式緩存
- 進程內緩存用於在單個進程中實現緩存時,當進程終止時,緩存會隨之消失。如果您在多個伺服器上運行相同的進程,則每個伺服器都有一個單獨的緩存。
- 持久化緩存是指在進程記憶體之外備份緩存,它可能位於文件中,也可能位於資料庫中。這實現比較困難,但如果重新啟動進程,緩存不會丟失。
- 分散式緩存是指您為多台電腦提供共用緩存,通常它將是幾個伺服器,使用分散式緩存,它存儲在外部服務中。這意味著如果一臺伺服器保存了緩存項,其他伺服器也可以使用它。像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中刪除項目:
- 緩存可能占用大量記憶體,最終導致記憶體不足異常和崩潰。
- 高記憶體消耗可導致GC壓力(又稱記憶體壓力)。在這種狀態下,垃圾收集器的工作量超出預期,會影響性能。
- 如果數據發生更改,可能需要刷新緩存,我們的緩存基礎架構應該支持這種能力。
為了處理這些問題,緩存框架具有驅逐策略(即刪除策略),這些是根據某些邏輯從緩存中刪除項目的規則,常見的驅逐政策是:
- 絕對過期策略將在一段固定的時間後從緩存中刪除一個項目。
- 如果未在固定的時間內訪問項目,則滑動過期策略將從緩存中刪除項目。因此,如果我將到期時間設置為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; } }
讓我們分析一下新增內容:
SizeLimit
加入了MemoryCacheOptions,
這會將基於大小的策略添加到緩存容器中。相反,我們需要在每個緩存條目上設置大小,在這種情況下,我們每次設置為1SetSize(1),
這意味著緩存限製為1024個項目。- 當我們達到大小限制時,應該刪除哪個緩存項?您實際上可以設置優先順序
.SetPriority(CacheItemPriority.High)
。級別為Low,Normal,High和NeverRemove。 SetSlidingExpiration(TimeSpan.FromSeconds(2))
添加了,將滑動到期時間設置為2秒,這意味著如果超過2秒內未訪問某個項目,它將被刪除。SetAbsoluteExpiration(TimeSpan.FromSeconds(10))
添加了,它將絕對到期時間設置為10秒,這意味著如果物品尚未在10秒內被驅逐。
除了示例中的選項之外,您還可以設置一個RegisterPostEvictionCallback
委托,當項目被驅逐時將調用該委托。
這是一個非常全面的功能集。它讓你想知道是否還有其他東西要添加,實際上有幾件事。
問題和缺失的功能
這個實現中有幾個重要的缺失部分。
- 雖然您可以設置大小限制,但緩存實際上並不監視gc壓力。如果我們確實對其進行監控,我們可以在壓力較大時收緊政策,併在壓力較低時放鬆政策。
- 當同時請求具有多個線程的相同項時,請求不等待第一個完成,該項目將被多次創建。例如,假設我們正在緩存阿凡達,從資料庫中獲取頭像需要10秒鐘,如果我們在第一次請求後2秒請求頭像,它將檢查頭像是否被緩存(它還沒有),並開始另一次訪問資料庫。
英文原文中有說明,但是覺得不太好,再次沒有翻譯。
英文原文地址:
代碼與所寫有所修改,但是大致意思一樣,如果感興趣,可以看看英文。