时间:2022-01-17 10:49:50 | 栏目:.NET代码 | 点击:次
缓存 。这是一个简单但非常有效的概念,这个想法的核心是记录过程数据,重用操作结果。当执行繁重的操作时,我们会将结果保存在我们的 缓存容器中 。下次我们需要该结果时,我们将从缓存容器中拉出它,而不是再次执行繁重的操作。
例如,要获取一个人的头像,您可能需要访问数据库。我们不会每次都执行那次旅行,而是将 Avatar 保存在缓存中,每次需要时从内存中提取它。
缓存非常适用于不经常更改的数据。或者甚至更好,永远不会改变。不断变化的数据,比如当前机器的时间不应该被缓存,否则你会得到错误的结果。
有 3 种类型的缓存:
我们将只讨论 进程内缓存 。
让我们用 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[]
) 保存在进程内存中。对头像的所有后续请求都将从内存中提取,从而节省时间和资源。
但是,正如编程中的大多数事情一样,没有什么是那么简单的。由于多种原因,上述解决方案并不好。一方面,这个实现 不是线程安全的
。从多个线程使用时可能会发生异常。除此之外,缓存的项目将永远留在内存中,这实际上非常糟糕。
这就是我们应该从缓存中删除项目的原因:
为了处理这些问题,缓存框架具有 驱逐策略 (又名 移除策略 )。这些是根据某些逻辑从缓存中删除项目的规则。常见的驱逐政策有:
现在我们知道我们需要什么,让我们继续寻找更好的解决方案。
作为一名博主,令我非常沮丧的是,微软已经创建了一个很棒的缓存实现。这剥夺了我自己创建类似实现的乐趣,但至少我写这篇博文的工作量减少了。
我将向您展示微软的解决方案,如何有效地使用它,然后在某些场景中如何改进它。
System.Runtime.Caching/MemoryCache 与 Microsoft.Extensions.Caching.Memory
Microsoft
有 2 个解决方案 2 个不同的 NuGet
包用于缓存。两者都很棒。根据 Microsoft
的 建议 ,更喜欢使用, Microsoft.Extensions.Caching.Memory 因为它与 Asp.NET Core
集成得更好。它可以很 容易地注入 到 Asp .NET Core
的依赖注入机制中。
这是一个基本示例 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 允许我们之前谈到的所有 驱逐政策 。
下面是一个例子:
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
委托,该委托将在项目被驱逐时调用。在这个实现中有几个重要的缺失部分。
事实上,这是一个 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));
此实现锁定项目的创建。锁是特定于钥匙的。例如,如果我们正在等待获取 Alex
的 Avatar
,我们仍然可以在另一个线程上获取 John
或 Sarah
的缓存值。
字典 _locks
存储了所有的锁。常规锁不适用于 async/await
,因此我们需要使用 SemaphoreSlim [5] .
如果 (!_cache.TryGetValue(key, out cacheEntry)),
有 2 次检查以查看该值是否已被缓存。锁内的那个是确保只有一个创建的那个。锁外面的那个是为了优化。
这个实现显然有一些开销。让我们考虑什么时候甚至有必要。
在以下情况下使用 WaitToFinishMemoryCache:
在以下情况下不要使用 WaitToFinishMemoryCache:
概括:
缓存是一种非常强大的模式,它也很危险,并且有其自身的复杂性。缓存太多,可能会导致 GC 压力,缓存太少会导致性能问题。而分布式缓存,这是一个需要探索的全新世界。软件开发职业就这样,总是有新的东西要学习。