软件开发中最常用的模式之一是缓存,这是一个简单但非常有效的概念,想法是重用操作结果,执行繁重的操作时,我们会将结果保存在缓存容器中,
下次我们需要该结果时,我们将从缓存容器中取出它,而不是再次执行繁重的操作。

例如,要获得某人的头像,您可能需要前往数据库。我们不会每次都执行那次查询,而是将结果保存在缓存中,每次需要时都将其从内存中删除。

缓存非常适合不经常更改的数据,甚至永远不会改变。不断变化的数据不适合缓存,如当前机器的时间不应缓存,否则您将得到错误的结果。

进程内缓存,持久化缓存和分布式缓存

* 进程内缓存用于在单个进程中实现缓存时,当进程终止时,缓存会随之消失。如果您在多个服务器上运行相同的进程,则每个服务器都有一个单独的缓存。
* 持久化缓存是指在进程内存之外备份缓存,它可能位于文件中,也可能位于数据库中。这实现比较困难,但如果重新启动进程,缓存不会丢失。

* 分布式缓存是指您为多台计算机提供共享缓存,通常它将是几个服务器,使用分布式缓存,它存储在外部服务中。
这意味着如果一台服务器保存了缓存项,其他服务器也可以使用它。像Redis <https://redis.io/>这样的服务非常适合这种情况。
单线程的缓存
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包用于缓存,两者都很棒,根据微软的建议
<https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2#systemruntimecachingmemorycache>
,更喜欢使用Microsoft.Extensions.Caching.Memory因为它与Asp更好地集成.NET核心。它可以很容易地注入
<https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2#using-imemorycache>
到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,这会将基于大小的策略添加到缓存容器中。相反,我们需要在每个缓存条目上设置大小,
在这种情况下,我们每次设置为1 SetSize(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秒请求头像,它将检查头像是否被缓存(它还没有),并开始另一次访问数据库。
英文原文中有说明,但是觉得不太好,再次没有翻译。

英文原文地址:


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

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

代码与所写有所修改,但是大致意思一样,如果感兴趣,可以看看英文。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信