C# .NET 中的緩存實現(xiàn)詳情
一、緩存的基本概念
緩存 。這是一個簡單但非常有效的概念,這個想法的核心是記錄過程數(shù)據(jù),重用操作結(jié)果。當執(zhí)行繁重的操作時,我們會將結(jié)果保存在我們的 緩存容器中 。下次我們需要該結(jié)果時,我們將從緩存容器中拉出它,而不是再次執(zhí)行繁重的操作。
例如,要獲取一個人的頭像,您可能需要訪問數(shù)據(jù)庫。我們不會每次都執(zhí)行那次旅行,而是將 Avatar 保存在緩存中,每次需要時從內(nèi)存中提取它。
緩存非常適用于不經(jīng)常更改的數(shù)據(jù)?;蛘呱踔粮?,永遠不會改變。不斷變化的數(shù)據(jù),比如當前機器的時間不應該被緩存,否則你會得到錯誤的結(jié)果。
二、緩存
有 3 種類型的緩存:
- In-Memory Cache: 用于在單個進程中實現(xiàn)緩存。當進程終止時,緩存也隨之終止。如果您在多臺服務器上運行相同的進程,您將為每臺服務器提供一個單獨的緩存。
- 持久性進程內(nèi)緩存: 是指在進程內(nèi)存之外備份緩存。它可能在文件中,也可能在數(shù)據(jù)庫中。這比較困難,但如果您的進程重新啟動,緩存不會丟失。最適合在獲取緩存項的情況下使用范圍廣泛,并且您的進程往往會重新啟動很多。
- 分布式緩存: 是指您希望為多臺機器共享緩存。通常,它將是多個服務器。使用分布式緩存,它存儲在外部服務中。這意味著如果一臺服務器保存了一個緩存項,其他服務器也可以使用它。像 Redis [1] 這樣的服務非常適合這一點。
我們將只討論 進程內(nèi)緩存 。
三、進程內(nèi)緩存早期做法
讓我們用 C# 創(chuàng)建一個非常簡單的緩存實現(xiàn):
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));
這個簡單的代碼解決了一個關(guān)鍵問題。要獲取用戶的頭像,只有第一個請求才會真正執(zhí)行到數(shù)據(jù)庫的訪問。然后將頭像數(shù)據(jù) ( byte[]
) 保存在進程內(nèi)存中。對頭像的所有后續(xù)請求都將從內(nèi)存中提取,從而節(jié)省時間和資源。
但是,正如編程中的大多數(shù)事情一樣,沒有什么是那么簡單的。由于多種原因,上述解決方案并不好。一方面,這個實現(xiàn) 不是線程安全的
。從多個線程使用時可能會發(fā)生異常。除此之外,緩存的項目將永遠留在內(nèi)存中,這實際上非常糟糕。
這就是我們應該從緩存中刪除項目的原因:
- 緩存會占用大量內(nèi)存,最終導致內(nèi)存不足異常和崩潰。
- 高內(nèi)存消耗會導致 GC 壓力 (又名內(nèi)存壓力)。在這種狀態(tài)下,垃圾收集器的工作量超出其應有的水平,從而損害了性能。
- 如果數(shù)據(jù)發(fā)生變化,可能需要刷新緩存。我們的緩存基礎設施應該支持這種能力。
為了處理這些問題,緩存框架具有 驅(qū)逐策略 (又名 移除策略 )。這些是根據(jù)某些邏輯從緩存中刪除項目的規(guī)則。常見的驅(qū)逐政策有:
- 無論如何, 絕對過期 策略將在固定時間后從緩存中刪除項目。
- 如果在固定的時間段內(nèi)未 訪問 某個項目,則 滑動過期 策略將從緩存中刪除該項目。因此,如果我將過期時間設置為 1 分鐘,只要我每 30 秒使用一次,該項目就會一直保留在緩存中。一旦我超過一分鐘不使用它,該物品就會被驅(qū)逐。
- 大小限制 策略將限制緩存內(nèi)存大小。
現(xiàn)在我們知道我們需要什么,讓我們繼續(xù)尋找更好的解決方案。
四、更好的解決方案
作為一名博主,令我非常沮喪的是,微軟已經(jīng)創(chuàng)建了一個很棒的緩存實現(xiàn)。這剝奪了我自己創(chuàng)建類似實現(xiàn)的樂趣,但至少我寫這篇博文的工作量減少了。
我將向您展示微軟的解決方案,如何有效地使用它,然后在某些場景中如何改進它。
System.Runtime.Caching/MemoryCache 與 Microsoft.Extensions.Caching.Memory
Microsoft
有 2 個解決方案 2 個不同的 NuGet
包用于緩存。兩者都很棒。根據(jù) Microsoft
的 建議 ,更喜歡使用, Microsoft.Extensions.Caching.Memory 因為它與 Asp.NET Core
集成得更好。它可以很 容易地注入 到 Asp .NET Core
的依賴注入機制中。
1、
這是一個基本示例 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
,所以有什么改變?嗯,一方面,這是一個 線程安全的 實現(xiàn)。您可以一次從多個線程安全地調(diào)用它。
第二件事是 MemoryCache 允許我們之前談到的所有 驅(qū)逐政策 。
下面是一個例子:
2、具有驅(qū)逐策略的 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
. 這為我們的緩存容器添加了基于大小的策略。大小沒有單位。相反,我們需要在每個緩存條目上設置大小數(shù)量。在這種情況下,我們每次將金額設置為1 SetSize(1)
。這意味著緩存限制為1024
個項目。- 當我們達到大小限制時,應該刪除哪個緩存項?您實際上可以使用 .
SetPriority(CacheItemPriority.High
) . 級別為 Low、Normal、High 和 NeverRemove
。 SetSlidingExpiration(TimeSpan.FromSeconds(2))
添加了,它將 滑動過期 時間設置為 2 秒。這意味著如果一個項目在 2 秒內(nèi)未被訪問,它將被刪除。-
SetAbsoluteExpiration(TimeSpan.FromSeconds(10))
添加了,將 絕對過期 時間設置為 10 秒。這意味著該項目將在 10 秒內(nèi)被驅(qū)逐,如果它還沒有。 - 除了示例中的選項之外,您還可以設置一個
RegisterPostEvictionCallback
委托,該委托將在項目被驅(qū)逐時調(diào)用。 - 這是一個非常全面的功能集。它讓你想知道是否還有什么要添加的。實際上有幾件事。
3、問題和缺失的功能
在這個實現(xiàn)中有幾個重要的缺失部分。
- 雖然您可以設置大小限制,但緩存實際上并不監(jiān)控 gc 壓力。如果真的監(jiān)測,壓力大的時候可以收緊政策,壓力小的時候可以放松政策。
- 當多個線程同時請求同一個項目時,請求不會等待第一個完成。該項目將被創(chuàng)建多次。例如,假設我們正在緩存頭像,從數(shù)據(jù)庫中獲取頭像需要 10 秒。如果我們在第一次請求后 2 秒請求頭像,它將檢查頭像是否已緩存(尚未緩存),并開始另一次訪問數(shù)據(jù)庫。
事實上,這是一個 MemoryCache
完全解決它的實現(xiàn):
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、代碼說明
此實現(xiàn)鎖定項目的創(chuàng)建。鎖是特定于鑰匙的。例如,如果我們正在等待獲取 Alex
的 Avatar
,我們?nèi)匀豢梢栽诹硪粋€線程上獲取 John
或 Sarah
的緩存值。
字典 _locks
存儲了所有的鎖。常規(guī)鎖不適用于 async/await
,因此我們需要使用 SemaphoreSlim [5] .
如果 (!_cache.TryGetValue(key, out cacheEntry)),
有 2 次檢查以查看該值是否已被緩存。鎖內(nèi)的那個是確保只有一個創(chuàng)建的那個。鎖外面的那個是為了優(yōu)化。
五、何時使用 WaitToFinishMemoryCache
這個實現(xiàn)顯然有一些開銷。讓我們考慮什么時候甚至有必要。
在以下情況下使用 WaitToFinishMemoryCache:
- 當項目的創(chuàng)建時間具有某種成本時,您希望盡可能減少創(chuàng)建。
- 當一個項目的創(chuàng)建時間很長時。
- 當必須確保每個鍵都創(chuàng)建一個項目時。
在以下情況下不要使用 WaitToFinishMemoryCache:
- 沒有多個線程訪問同一個緩存項的危險。
- 您不介意多次創(chuàng)建該項目。例如,如果對數(shù)據(jù)庫的額外訪問不會有太大變化。
概括:
緩存是一種非常強大的模式,它也很危險,并且有其自身的復雜性。緩存太多,可能會導致 GC 壓力,緩存太少會導致性能問題。而分布式緩存,這是一個需要探索的全新世界。軟件開發(fā)職業(yè)就這樣,總是有新的東西要學習。
到此這篇關(guān)于C# .NET 中的緩存實現(xiàn)詳情的文章就介紹到這了,更多相關(guān)C# .NET 中的緩存實現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#常用數(shù)據(jù)結(jié)構(gòu)棧的詳細介紹
在C#中,Stack<T> 是一個后進先出(LIFO,Last-In-First-Out)集合類,位于System.Collections.Generic 命名空間中,本文詳細介紹C#常用數(shù)據(jù)結(jié)構(gòu)棧,感興趣的朋友跟隨小編一起看看吧2024-09-09使用C#實現(xiàn)阿拉伯數(shù)字到大寫中文的轉(zhuǎn)換
這篇文章主要介紹了C#實現(xiàn)阿拉伯數(shù)字轉(zhuǎn)為大寫中文的實現(xiàn)代碼,需要的朋友可以參考下2007-03-03WPF利用CommunityToolkit.Mvvm實現(xiàn)級聯(lián)選擇器
這篇文章主要介紹了WPF如何利用CommunityToolkit.Mvvm實現(xiàn)級聯(lián)選擇器,文中的示例代碼講解詳細,對我們的學習或工作有一定幫助,需要的小伙伴可以參考一下2023-12-12