C#.NET中的缓存实现软件开发中最常见的模式之一是缓存。这是一个简单但非常有效的概念,其核心思想是记录过程数据并重用操作结果。当执行繁重的操作时,我们将结果保存在我们的缓存容器中。下次我们需要该结果时,我们将从缓存容器中提取它,而不是再次执行繁重的工作。例如,要获取某个人的个人资料图片,您可能需要访问数据库。我们不是每次都执行该行程,而是将Avatar保存在缓存中,并在每次需要时从内存中获取它。缓存非常适合不经常更改的数据。甚至更好,永不改变。更改数据如当前机器时间不应该被缓存,否则你会得到错误的结果。In-ProcessCache,PersistentIn-ProcessCache,andDistributedCache缓存有3种类型:In-MemoryCache用于在单个进程中实现缓存。当进程终止时,缓存也终止。如果您在多台服务器上运行相同的进程,则每台服务器都会有一个单独的缓存。持久进程内缓存是支持进程内存之外的缓存的缓存。它可以在文件中,也可以在数据库中。这更难,但是如果您的进程重新启动,缓存不会丢失。最适合在获取缓存项的范围很广,并且您的进程往往会重新启动很多的情况下使用。分布式缓存是当您想要为多台机器共享缓存时。通常,它将是多个服务器。使用分布式缓存,它存储在外部服务中。这意味着如果一个服务器保存了缓存条目,其他服务器也可以使用它。像Redis[1]这样的服务非常适合这个。我们将只讨论进程内缓存。早期的方法让我们在C#中创建一个非常简单的缓存实现:_cache.ContainsKey(key)){_cache[key]=createItem();}return_cache[key];}}用法:var_avatarCache=newNaiveCache();//...varmyAvatar=_avatarCache.GetOrCreate(userId,()=>_database.GetAvatar(userId));这段简单的代码解决了一个关键问题。要获取用户的头像,只有第一个请求实际上对数据库执行了一次命中。然后将头像数据(byte[])保存在进程内存中。所有对头像的后续请求都将从内存中获取,从而节省时间和资源。但是,就像编程中的大多数事情一样,没有什么是那么简单的。出于多种原因,上述解决方案并不好。一方面,这个实现不是线程安全的。从多个线程使用时可能会发生异常。除此之外,缓存项将永远保留在内存中,这实际上是非常糟糕的。下面是为什么我们应该从缓存中移除项:1.缓存消耗大量内存,最终导致内存不足异常和崩溃。2.高内存消耗会导致GC压力(akamemorypressure)。在这种状态下,垃圾收集器的工作量超出了应有的范围,从而损害了性能。3.如果数据发生变化,可能需要刷新缓存。我们的缓存基础设施应该支持这种能力。为了处理这些问题,缓存框架有逐出策略(又名逐出策略)。这些是根据某些逻辑从缓存中删除项目的规则。常见的逐出策略是:无论如何,绝对过期策略将在固定时间后从缓存中删除项目。如果某个项目在固定时间段内未被访问,则滑动过期策略会从缓存中删除该项目。因此,如果我将过期时间设置为1分钟,只要我每30秒使用一次,该项目就会保留在缓存中。一旦我超过一分钟不使用它,它就会被驱逐。大小限制策略将限制缓存内存大小。现在我们知道我们需要什么,让我们继续寻找更好的解决方案。更好的解决方案作为博主,令我非常沮丧的是,Microsoft创建了一个很棒的缓存实现。这带走了我自己创建类似实现的乐趣,但至少我写这篇博文的工作量减少了。我将向您展示微软的解决方案,如何有效地使用它,然后在某些场景下如何改进它。System.Runtime.Caching/MemoryCache与Microsoft.Extensions.Caching.MemoryMicrosoft有2种解决方案和2种不同的NuGet缓存包。两者都很棒。正如Microsoft[2]所推荐的,更喜欢使用Microsoft.Extensions.Caching.Memory,因为它与Asp.NETCore集成得更好。它可以很容易地注入[3]到Asp.NETCore的依赖注入机制中。下面是Microsoft.Extensions.Caching.Memory的一个基本示例:))//Lookforcachekey.{//Keynotincache,sogetdata.cacheEntry=createItem();//Savedataincache._cache.Set(key,cacheEntry);}returncacheEntry;}}用法:var_avatarCache=newSimpleMemoryCache();//...varmyAvatar=_avatarCache.GetOrCreate(userId,()=>_database.GetAvatar(userId));这与我自己的NaiveCache非常相似,那么有什么变化呢?好吧,一方面,这是一个线程安全的实现。您可以同时从多个线程安全地调用它。第二件事是MemoryCache允许我们之前谈到的所有驱逐政策。下面是一个示例:IMemoryCache带逐出策略:publicclassMemoryCacheWithPolicy{privateMemoryCache_cache=newMemoryCache(newMemoryCacheOptions(){SizeLimit=1024});publicTItemGetOrCreate(objectkey,FunccreateueItem){TItemcacheValcacheEntry(Get_ValcacheEntry;if(!key,outcacheEntry))//寻找cachekey.{//Keynotincache,sogetdata.cacheEntry=createItem();varcacheEntryOptions=newMemoryCacheEntryOptions().SetSize(1)//Sizeamount//Priorityonremovingwhenreachingsizelimit(memorypressure).SetPriority(Cache.HighPrior)}1.MemoryCacheOptions增加了SizeLimit。这为我们的缓存容器添加了一个基于大小的策略。大小没有单位。相反,我们需要在每个缓存条目上设置大小。在这种情况下,我们每次SetSize(1)时都将金额设置为1。这意味着缓存限制为1024个项目。2.当我们达到大小限制时应该删除哪个缓存项?您实际上可以使用.SetPriority(CacheItemPriority.High)。级别为低、正常、高和NeverRemove。3.新增SetSlidingExpiration(TimeSpan.FromSeconds(2)),设置滑动过期时间为2秒。这意味着如果一个项目在2秒内没有被访问,它将被删除。4.SetAbsoluteExpiration(TimeSpan.FromSeconds(10))添加,设置绝对过期时间为10秒。这意味着该项目将在10秒内被驱逐(如果尚未驱逐的话)。除了示例中的选项之外,您还可以设置将在逐出项目时调用的RegisterPostEvictionCallback委托。这是一个非常全面的功能集。这让您想知道是否还有其他要添加的内容。实际上有几件事。问题和缺失的特性在这个实现中有几个重要的缺失部分。1.虽然您可以设置大小限制,但缓存实际上并不监控gc压力。如果真的监控到,压力大的时候可以收紧政策,压力小的时候可以放松政策。2.当多个线程同时请求同一个item时,请求不会等待第一个完成。该项目将被创建多次。例如,假设我们正在缓存头像,需要10秒才能从数据库中获取它们。如果我们在第一次请求后2秒请求头像,它将检查头像是否被缓存(它没有)并开始对数据库的另一次点击。关于GC压力的第一个问题:可以使用各种技术和启发式方法来监控GC压力。这篇博文与此处无关,但您可以阅读我的文章在C#.NET中查找、修复和避免内存泄漏:8项最佳实践[4],了解一些有用的方法。第二个问题比较容易解决。实际上,这是完全解决它的MemoryCache的实现:publicclassWaitToFinishMemoryCache{privateMemoryCache_cache=newMemoryCache(newMemoryCacheOptions());privateConcurrentDictionary