MemoryCache 是 C# 应用程序的内存中缓存的标准实现。它有助于提高性能和可扩展性,但代价是内存消耗增加。本文介绍了有关何时以及如何使用 .MemoryCache
内存中缓存几乎适用于所有类型的应用程序:移动、服务器端或客户端 Web、云和桌面。当计算或请求是资源密集型或耗时的,并且以后可能会重复使用时,它特别有用。将缓存视为抵御高额计算和云消耗成本的第一道防线。
作为一般准则,如果操作至少具有以下属性中的三个,则应缓存该操作:
例如:
为了说明缓存的用例,我们将使用一个小型的 ASP.NET Core 应用程序,它显示两种法定货币和两种加密货币的价格。
我们的目标是向网页用户显示以美元计价的当前比特币和以太坊价格数据。因此,我们查询从 CoinCap 提供实时数据的 API 并为用户呈现数据。
模型类中的方法从 HTTP 服务中检索其数据。依赖项是使用主构造函数拉取的。GetCurrencyDataIHttpClientFactory
public class NoCachingModel( IHttpClientFactory httpClientFactory )
: BaseModel
{
public async Task<CoinCapData> GetCurrencyData( string id )
{
using var httpClient = httpClientFactory.CreateClient();
var response = await httpClient.GetFromJsonAsync<CoinCapResponse>(
$"https://api.coincap.io/v2/rates/{id}" );
return response!.Data;
}
}
货币列表在 base model class 中定义。
public class BaseModel : PageModel
{
public IReadOnlyList<string> Currencies { get; } =
[
"bitcoin",
"ethereum",
"euro",
"british-pound-sterling"
];
}
以下是显示货币的页面代码:
@page "/"
@using System.Globalization
@model NoCachingModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
@{
foreach (var currency in Model.Currencies)
{
var data = await Model.GetCurrencyData( currency );
// Output a paragraph with symbol and rate.
@:<p>@( data.Symbol )(@data.Type):
@data.RateUsd.ToString( "F3", CultureInfo.InvariantCulture )
</p>
}
}
</div>
顺便说一句,我们已经包含了使用 ASP.NET 中间件的代码来监控呈现页面所需的时间。这将在页面上显示内部处理时间,我们将在整篇文章中引用:
processed in 397 ms.
但是,我们页面的每个视图都会触发来自服务的四次数据检索。这会导致几个问题:
我们可以通过雇用 来解决所有这些问题。MemoryCache
MemoryCache_非常快_。缓存的数据驻留在使用它们的同一进程中,并存储为常规 .NET 对象。所做的只是存储对对象的_引用_,将其与键关联并防止它被垃圾回收。MemoryCache
相比之下,对于像 Redis 这样的分布式缓存服务器,缓存位于不同的机器上,通常是专用机器,并在多台服务器之间共享。分布式缓存不存储 .NET 对象,而是存储二进制或文本(通常为 JSON 或 XML)数据。因此,.NET 对象应在存储到缓存中之前进行序列化,并在检索后进行反序列化。使用 ,序列化和反序列化不是必需的。MemoryCache
那么,有多快呢?在我的开发笔记本上,我测量了:MemoryCache
这意味着,即使您的应用程序每秒和每个实例处理数万个请求,您也不必担心其性能。MemoryCache
即使它非常快_,也不是 C_# 最快的缓存。但是,它在功能和性能之间提供了良好的平衡。MemoryCache
该类包含在包中。此软件包自动包含在 ASP.NET Core 应用程序中。如果要构建不同类型的应用程序,则可能需要将此包添加到项目中。通常,我们不会直接针对类编写代码,而是针对 的 抽象编写代码。MemoryCacheMicrosoft.Extensions.Caching.MemoryMemoryCacheIMemoryCache
在最低级别,该接口提供以下基本方法:IMemoryCache
大多数时候,我们不会在代码中使用这些基本方法,而是使用 high level 或 extension 方法。给定一个缓存键,这些方法将:GetOrCreateGetOrCreateAsync
首先,我们需要将内存缓存组件添加到我们的服务集合中。
在 中,将内存缓存添加到如下所示:Program.csWebApplicationBuilder
builder.Services.AddMemoryCache();
下一步是将 注入到你的页面或模型中。IMemoryCache
如果要直接将代码添加到页面,请使用以下指令将 注入到我们的页面模型中:IMemoryCache
@inject IMemoryCache Cache
如果要编写纯 C# 而不是 Razor,请拉取该服务。IMemoryCache
public class WithMemoryCacheModel(
IHttpClientFactory httpClientFactory, IMemoryCache memoryCache )
: BaseModel
现在,您可以通过添加对 .IMemoryCache.GetOrCreateAsync
一种方便的模式是将原始方法逻辑移动到本地函数,如下面的代码片段所示。
public async Task<CoinCapData> GetCurrencyData( string id )
{
return (await memoryCache.GetOrCreateAsync(
$"{this.GetType().Name}.GetCurrencyData({id})",
_ => GetData() ))!;
async Task<CoinCapData> GetData()
{
using var httpClient = httpClientFactory.CreateClient();
var response = await httpClient.GetFromJsonAsync<CoinCapResponse>(
$"https://api.coincap.io/v2/rates/{id}" );
return response!.Data;
}
}
每次调用该方法时,都会检查缓存。如果数据可用,则检索数据。否则,将调用本地函数,并将结果存储在缓存中。当用户访问我们的网站时,第一个请求会使用 的结果填充缓存,所有后续请求都会使用此数据。这种方法可显著降低用户延迟、降低 CPU 使用率并降低外部 API 使用率。因此,我们改善了用户体验,节省了成本,并增强了可靠性,所有这些都是为了一个小的内存权衡。GetData
请务必注意,所有方法参数都应添加到缓存键中。此外,我们需要确保我们方法的缓存键不会与其他方法的缓存键发生冲突。
运行该应用程序,我们可以看到示例的本地延迟现在已经从之前的 397 毫秒下降到 1 毫秒。
在上面的示例中,我们成功地改善了服务的延迟和可靠性。但是,我们无意中使其变得无用,因为它现在显示不变价格。我们已将数据添加到缓存中,但尚未实现任何刷新数据的机制。此机制称为 过期 或 过期。
假设我们希望数据最多为 30 秒。
在 中,我们追求的机制称为_绝对过期_。我们可以通过调用 provide 的对象的方法来设置它。MemoryCacheSetAbsoluteExpirationICacheEntryGetOrCreateAsync
public async Task<CoinCapData> GetCurrencyData( string id )
{
return
(await memoryCache.GetOrCreateAsync(
$"{this.GetType().Name}.{id}",
async entry =>
{
entry.SetAbsoluteExpiration( TimeSpan.FromSeconds( 30 ) );
return await GetData();
} ))!;
async Task<CoinCapData> GetData()
{
using var httpClient = httpClientFactory.CreateClient();
var response = await httpClient.GetFromJsonAsync<CoinCapResponse>(
$"https://api.coincap.io/v2/rates/{id}" );
return response!.Data;
}
}
现在,将在创建条目大约 30 秒后删除该条目。大多数请求仍将具有 1 毫秒的延迟,但每 30 秒一个请求会很慢,因为它需要命中货币服务。MemoryCache
对应用程序进行硬编码以使用并不总是最佳方法。如果将来可能想要更改缓存策略,请考虑在源代码和缓存组件之间使用中间层。IMemoryCache
Polly 是一个常用的 .NET 包,可帮助处理暂时性故障并增强应用程序弹性。其中一个可用策略是 ,它在内部使用 .Polly 提供了其他几个有用的策略。例如,如果操作失败,您可能希望重试该操作。对于 Polly,这很简单。Polly 还可以更轻松地从分布式缓存切换到分布式缓存。Polly.Caching.MemoryIMemoryCacheMemoryCache
让我们看看如何使用 Polly 为您的应用程序添加缓存。
要进行设置,我们需要在应用程序初始化期间添加以下代码:
builder.Services
.AddSingleton<IAsyncCacheProvider, MemoryCacheProvider>();
builder.Services
.AddSingleton<IReadOnlyPolicyRegistry<string>, PolicyRegistry>(
serviceProvider =>
{
var cachingPolicy = Policy.CacheAsync(
serviceProvider.GetRequiredService<IAsyncCacheProvider>(),
TimeSpan.FromMinutes(0.5));
var registry = new PolicyRegistry
{ ["defaultPolicy"] = cachingPolicy };
return registry;
});
接下来,我们需要将 resilience 策略合并到我们的模型中:
private readonly IAsyncPolicy _cachePolicy;
private IHttpClientFactory _httpClientFactory;
public Step4Model( IReadOnlyPolicyRegistry<string> policyRegistry,
IHttpClientFactory httpClientFactory )
{
this._httpClientFactory = httpClientFactory;
this._cachePolicy = policyRegistry.Get<IAsyncPolicy>( "defaultPolicy" );
}
最后,我们可以在我们的方法中使用 cache 策略:GetCurrencyData
public async Task<CoinCapData> GetCurrencyData( string id )
{
return await this._cachePolicy.ExecuteAsync(
async _ => await GetData(),
new Context( $"{this.GetType().Name}.GetCurrencyData({id})" ) );
async Task<CoinCapData> GetData()
{
using var httpClient = this._httpClientFactory.CreateClient();
var response = await httpClient.GetFromJsonAsync<CoinCapResponse>(
$"https://api.coincap.io/v2/rates/{id}" );
return response!.Data;
}
}
如果以后想要添加一些弹性功能,只需编辑初始化代码即可。在下一个代码段中,我们将向缓存策略添加重试策略。
builder.Services
.AddSingleton<IAsyncCacheProvider, MemoryCacheProvider>();
builder
.Services.AddSingleton<IReadOnlyPolicyRegistry<string>, PolicyRegistry>(
serviceProvider =>
{
var cachingPolicy = Policy.CacheAsync(
serviceProvider.GetRequiredService<IAsyncCacheProvider>(),
TimeSpan.FromMinutes( 0.5 ) );
var retryPolicy = Policy.Handle<Exception>().RetryAsync();
var policy = Policy.WrapAsync( cachingPolicy, retryPolicy );
var registry = new PolicyRegistry { ["defaultPolicy"] = policy };
return registry;
} );
直接在代码中使用的另一种替代方法是 EasyCaching。EasyCaching 的主要优点是它为多个缓存服务器提供实现,使您可以根据需要更轻松地迁移到分布式缓存。它还处理 .NET 对象从二进制或文本数据到二进制或文本数据的序列化和反序列化。MemoryCache
您可能已经注意到,上面的示例需要大量的重复代码,因为每个方法都需要包装在委托调用中并生成缓存键。
通过使用 Metalama(一种用于 C# 的代码生成和验证工具包),可以消除这种重复的代码。如果您只想将其用于缓存,免费版就足够了。
有两种方法可以使用 Metalama 实现缓存。
一种是手动编写代码模板(称为 aspect)。这超出了本文的范围,您可以查看缓存示例以了解更多信息。
第二种方法是使用已经实现该方面的 Metalama.Patterns.Caching.Aspects 包。
初始设置很简单,在我们的Program.cs只需要两行代码:
builder.Services.AddMetalamaCaching();
然后,使用 the 属性标记方法:[Cache]
[Cache( AbsoluteExpiration = 0.5 )]
public async Task<CoinCapData> GetCurrencyData( string id )
{
using var httpClient = httpClientFactory.CreateClient();
var response = await httpClient.GetFromJsonAsync<CoinCapResponse>(
$"https://api.coincap.io/v2/rates/{id}" );
return response!.Data;
}
就这样!在构建项目时,Metalama 会生成所有必要的代码,而无需我们修改方法中的任何内容。
与 EasyCaching 一样,它与缓存提供程序无关。它还处理序列化,如果你尝试缓存不可缓存的内容(例如,枚举器),你的构建将失败。与其他解决方案不同,它处理了生成缓存密钥的麻烦。Metalama.Patterns.Caching
MemoryCache是内存中缓存的 C# 实现。您可以使用它来提高应用程序性能并减少其资源消耗,但代价是内存使用量增加。我们讨论了不适合使用缓存的情况 - 要么是由于操作的性质,要么是由于部署的拓扑。
尽管您可以直接在代码中使用,但如果您想保持源代码的健壮性和可维护性,这可能不是最佳选择。使用 Polly、EasyCache 或 Metalama Caching 等中间层将使将来的代码发展变得更加容易。此外,Metalama 显著减少了您需要编写的代码量,因为它归结为单个自定义属性。