.NET 中的缓存

您所在的位置:网站首页 缓存的三种类型 .NET 中的缓存

.NET 中的缓存

2024-06-05 18:08| 来源: 网络整理| 查看: 265

.NET 中的缓存 项目 04/11/2024

本文介绍各种缓存机制。 缓存指在中间层中存储数据的行为,该行为可使后续数据检索更快。 从概念上讲,缓存是一种性能优化策略和设计考虑因素。 缓存可以显著提高应用性能,方法是提高不常更改(或检索成本高)的数据的就绪性。 本文介绍两种主要的缓存,并提供这两种的示例源代码:

Microsoft.Extensions.Caching.Memory Microsoft.Extensions.Caching.Distributed

重要

.NET 有两个 MemoryCache 类,一个在 System.Runtime.Caching 命名空间中,另一个在 Microsoft.Extensions.Caching 命名空间中:

System.Runtime.Caching.MemoryCache Microsoft.Extensions.Caching.Memory.MemoryCache

虽然本文重点介绍缓存,但不包括 System.Runtime.Caching NuGet 包。 所有对 MemoryCache 的引用都在 Microsoft.Extensions.Caching 命名空间内。

所有 Microsoft.Extensions.* 包都具有依赖项注入 (DI) 就绪性,并且 IMemoryCache 和 IDistributedCache 接口都可以用作服务。

内存中缓存

本部分将介绍 Microsoft.Extensions.Caching.Memory 包。 IMemoryCache 的当前实现是 ConcurrentDictionary 的包装器,公开功能丰富的 API。 缓存中的项由 ICacheEntry 表示,可以是任何 object。 内存中缓存解决方案适用于在单个服务器中运行的应用,其中所有缓存数据在应用进程中租用内存。

提示

对于多服务器缓存场景,请考虑使用分布式缓存方法替代内存中缓存。

内存中缓存 API

缓存的使用者可控制可调过期和绝对过期:

ICacheEntry.AbsoluteExpiration ICacheEntry.AbsoluteExpirationRelativeToNow ICacheEntry.SlidingExpiration

设置过期后,如果未在过期时间安排内访问缓存中的项,将导致这些项被逐出。 使用者可通过 MemoryCacheEntryOptions 使用其他选项来控制缓存项。 每个 ICacheEntry 都与 MemoryCacheEntryOptionMemoryCacheEntryOptions 配对,后者使用 IChangeToken 公开过期逐出功能,使用 CacheItemPriority 设置优先级,并控制 ICacheEntry.Size。 请考虑以下扩展方法:

MemoryCacheEntryExtensions.AddExpirationToken MemoryCacheEntryExtensions.RegisterPostEvictionCallback MemoryCacheEntryExtensions.SetSize MemoryCacheEntryExtensions.SetPriority 内存中缓存示例

若要使用默认 IMemoryCache 实现,请调用 AddMemoryCache 扩展方法,以向 DI 注册所有必需的服务。 在下面的代码示例中,泛型主机用于公开 DI 功能:

using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Services.AddMemoryCache(); using IHost host = builder.Build();

可以以不同方式访问 IMemoryCache,例如构造函数注入,具体取决于 .NET 工作负载。 在此示例中,在 host 上使用 IServiceProvider 实例,并调用泛型 GetRequiredService(IServiceProvider) 扩展方法:

IMemoryCache cache = host.Services.GetRequiredService();

注册内存中缓存服务并通过 DI 解析后,即可开始缓存。 此示例循环访问英文字母表“A”到“Z”中的字母。 record AlphabetLetter 类型保存对字母的引用并生成消息。

file record AlphabetLetter(char Letter) { internal string Message => $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet."; }

提示

file 访问修饰符用于 AlphabetLetter 类型,因为它在 Program.cs 文件中定义且仅从此文件中访问。 有关详细信息,请参阅文件(C# 参考)。 若要查看完整的源代码,请参阅 Program.cs 部分。

该示例包含一个帮助程序函数,该函数会循环访问字母表字母:

static async ValueTask IterateAlphabetAsync( Func asyncFunc) { for (char letter = 'A'; letter { MemoryCacheEntryOptions options = new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration) }; _ = options.RegisterPostEvictionCallback(OnPostEviction); AlphabetLetter alphabetLetter = cache.Set( letter, new AlphabetLetter(letter), options); Console.WriteLine($"{alphabetLetter.Letter} was cached."); return Task.Delay( TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd)); }); await addLettersToCacheTask;

在前述 C# 代码中:

变量 addLettersToCacheTask 委托给 IterateAlphabetAsync 并等待。 Func asyncFunc 使用 lambda 来表示。 MemoryCacheEntryOptions 是以相对于现在的绝对过期来实例化的。 逐出后回叫已注册。 AlphabetLetter 对象已实例化,并随 letter 和 options 一起传递到 Set 中。 该字母缓存时写入控制台。 最后,将返回 Task.Delay。

对于字母表中的每个字母,将写入一个包含过期和逐出后回叫的缓存项。

逐出后回叫会将逐出的值的详细信息写入控制台:

static void OnPostEviction( object key, object? letter, EvictionReason reason, object? state) { if (letter is AlphabetLetter alphabetLetter) { Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}."); } };

填充缓存后,将等待另一个对 IterateAlphabetAsync 的调用,但这次将调用 IMemoryCache.TryGetValue:

var readLettersFromCacheTask = IterateAlphabetAsync(letter => { if (cache.TryGetValue(letter, out object? value) && value is AlphabetLetter alphabetLetter) { Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}"); } return Task.CompletedTask; }); await readLettersFromCacheTask;

如果 cache 包含 letter 键,并且 value 是 AlphabetLetter 的实例,该键将写入控制台。 如果 letter 键不在缓存中,会逐出该键并调用其逐出后回叫。

其他扩展方法

IMemoryCache 具有许多方便的扩展方法,其中包括异步 GetOrCreateAsync:

CacheExtensions.Get CacheExtensions.GetOrCreate CacheExtensions.GetOrCreateAsync CacheExtensions.Set CacheExtensions.TryGetValue 将其放在一起

整个示例应用源代码是一个顶级程序,需要两个 NuGet 包:

Microsoft.Extensions.Caching.Memory Microsoft.Extensions.Hosting using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Services.AddMemoryCache(); using IHost host = builder.Build(); IMemoryCache cache = host.Services.GetRequiredService(); const int MillisecondsDelayAfterAdd = 50; const int MillisecondsAbsoluteExpiration = 750; static void OnPostEviction( object key, object? letter, EvictionReason reason, object? state) { if (letter is AlphabetLetter alphabetLetter) { Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}."); } }; static async ValueTask IterateAlphabetAsync( Func asyncFunc) { for (char letter = 'A'; letter { MemoryCacheEntryOptions options = new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration) }; _ = options.RegisterPostEvictionCallback(OnPostEviction); AlphabetLetter alphabetLetter = cache.Set( letter, new AlphabetLetter(letter), options); Console.WriteLine($"{alphabetLetter.Letter} was cached."); return Task.Delay( TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd)); }); await addLettersToCacheTask; var readLettersFromCacheTask = IterateAlphabetAsync(letter => { if (cache.TryGetValue(letter, out object? value) && value is AlphabetLetter alphabetLetter) { Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}"); } return Task.CompletedTask; }); await readLettersFromCacheTask; await host.RunAsync(); file record AlphabetLetter(char Letter) { internal string Message => $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet."; }

可以调整 MillisecondsDelayAfterAdd 和 MillisecondsAbsoluteExpiration 值,以观察缓存项过期和逐出行为的变化。 下面是运行此代码的示例输出。 由于 .NET 事件的不确定性,输出可能有所不同。

A was cached. B was cached. C was cached. D was cached. E was cached. F was cached. G was cached. H was cached. I was cached. J was cached. K was cached. L was cached. M was cached. N was cached. O was cached. P was cached. Q was cached. R was cached. S was cached. T was cached. U was cached. V was cached. W was cached. X was cached. Y was cached. Z was cached. A was evicted for Expired. C was evicted for Expired. B was evicted for Expired. E was evicted for Expired. D was evicted for Expired. F was evicted for Expired. H was evicted for Expired. K was evicted for Expired. L was evicted for Expired. J was evicted for Expired. G was evicted for Expired. M was evicted for Expired. N was evicted for Expired. I was evicted for Expired. P was evicted for Expired. R was evicted for Expired. O was evicted for Expired. Q was evicted for Expired. S is still in cache. The 'S' character is the 19 letter in the English alphabet. T is still in cache. The 'T' character is the 20 letter in the English alphabet. U is still in cache. The 'U' character is the 21 letter in the English alphabet. V is still in cache. The 'V' character is the 22 letter in the English alphabet. W is still in cache. The 'W' character is the 23 letter in the English alphabet. X is still in cache. The 'X' character is the 24 letter in the English alphabet. Y is still in cache. The 'Y' character is the 25 letter in the English alphabet. Z is still in cache. The 'Z' character is the 26 letter in the English alphabet.

由于已设置绝对过期 (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow),因此最终将逐出所有缓存项。

辅助角色服务缓存

缓存数据的一种常见策略是独立于使用数据服务更新缓存。 辅助角色服务模板是一个很好的示例,因为 BackgroundService 独立于其他应用程序代码(或在后台)运行。 当托管 IHostedService 实现的应用程序开始运行时,相应的实现(在这种情况下为 BackgroundService 或“辅助角色”)开始在同一进程中运行。 这些托管服务通过 AddHostedService(IServiceCollection) 扩展方法向 DI 注册为单一实例。 可以使用任何服务生存期向 DI 注册其他服务。

重要

请务必要了解服务生存期。 调用 AddMemoryCache 以注册所有内存中缓存服务时,服务将注册为单一实例。

照片服务场景

假设你在开发依赖于可通过 HTTP 访问的第三方 API 的照片服务。 这种照片数据不会经常更改,但数据量很大。 每张照片都由一个简单的 record 表示:

namespace CachingExamples.Memory; public readonly record struct Photo( int AlbumId, int Id, string Title, string Url, string ThumbnailUrl);

在下面的示例中,你将看到若干在 DI 中注册的服务。 每个服务承担单一责任。

using CachingExamples.Memory; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Services.AddMemoryCache(); builder.Services.AddHttpClient(); builder.Services.AddHostedService(); builder.Services.AddScoped(); builder.Services.AddSingleton(typeof(CacheSignal)); using IHost host = builder.Build(); await host.StartAsync();

在前述 C# 代码中:

泛型主机使用默认值创建。 内存中缓存服务使用 AddMemoryCache 注册。 使用 AddHttpClient(IServiceCollection) 为 CacheWorker 类注册了一个 HttpClient 实例。 CacheWorker 类使用 AddHostedService(IServiceCollection) 注册。 PhotoService 类使用 AddScoped(IServiceCollection) 注册。 CacheSignal 类使用 AddSingleton 注册。 host 由生成器实例化,并异步启动。

PhotoService 负责获取符合给定条件(或 filter)的照片:

using Microsoft.Extensions.Caching.Memory; namespace CachingExamples.Memory; public sealed class PhotoService( IMemoryCache cache, CacheSignal cacheSignal, ILogger logger) { public async IAsyncEnumerable GetPhotosAsync(Func? filter = default) { try { await cacheSignal.WaitAsync(); Photo[] photos = (await cache.GetOrCreateAsync( "Photos", _ => { logger.LogWarning("This should never happen!"); return Task.FromResult(Array.Empty()); }))!; // If no filter is provided, use a pass-thru. filter ??= _ => true; foreach (Photo photo in photos) { if (!default(Photo).Equals(photo) && filter(photo)) { yield return photo; } } } finally { cacheSignal.Release(); } } }

在前述 C# 代码中:

构造函数需要 IMemoryCache、CacheSignal 和 ILogger。 GetPhotosAsync 方法: 定义 Func filter 参数,并返回 IAsyncEnumerable。 调用 _cacheSignal.WaitAsync() 并等待它释放,这可确保在访问缓存前先填充缓存。 调用 _cache.GetOrCreateAsync(),异步获取缓存中的所有照片。 factory 参数记录警告,并返回空照片数组 - 这应该永远不会发生。 缓存中的每张照片都使用 yield return 进行循环访问、筛选和具体化。 最后,缓存信号已重置。

此服务的使用者可以调用 GetPhotosAsync 方法,并相应地处理照片。 不需要 HttpClient,因为缓存包含照片。

异步信号在一个泛型类型受约束的单一实例中基于一个封装的 SemaphoreSlim 实例。 CacheSignal 依赖于 SemaphoreSlim 实例:

namespace CachingExamples.Memory; public sealed class CacheSignal { private readonly SemaphoreSlim _semaphore = new(1, 1); /// /// Exposes a that represents the asynchronous wait operation. /// When signaled (consumer calls ), the /// is set as . /// public Task WaitAsync() => _semaphore.WaitAsync(); /// /// Exposes the ability to signal the release of the 's operation. /// Callers who were waiting, will be able to continue. /// public void Release() => _semaphore.Release(); }

在前述 C# 代码中,修饰器模式用于包装 SemaphoreSlim 的实例。 由于 CacheSignal 注册为单一实例,因此它可以在所有服务生存期中与任何泛型类型(在本例中为 Photo)一起使用。 它负责发出缓存的种子设定的信号。

CacheWorker 是 BackgroundService 的子类:

using System.Net.Http.Json; using Microsoft.Extensions.Caching.Memory; namespace CachingExamples.Memory; public sealed class CacheWorker( ILogger logger, HttpClient httpClient, CacheSignal cacheSignal, IMemoryCache cache) : BackgroundService { private readonly TimeSpan _updateInterval = TimeSpan.FromHours(3); private bool _isCacheInitialized = false; private const string Url = "https://jsonplaceholder.typicode.com/photos"; public override async Task StartAsync(CancellationToken cancellationToken) { await cacheSignal.WaitAsync(); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { logger.LogInformation("Updating cache."); try { Photo[]? photos = await httpClient.GetFromJsonAsync( Url, stoppingToken); if (photos is { Length: > 0 }) { cache.Set("Photos", photos); logger.LogInformation( "Cache updated with {Count:#,#} photos.", photos.Length); } else { logger.LogWarning( "Unable to fetch photos to update cache."); } } finally { if (!_isCacheInitialized) { cacheSignal.Release(); _isCacheInitialized = true; } } try { logger.LogInformation( "Will attempt to update the cache in {Hours} hours from now.", _updateInterval.Hours); await Task.Delay(_updateInterval, stoppingToken); } catch (OperationCanceledException) { logger.LogWarning("Cancellation acknowledged: shutting down."); break; } } } }

在前述 C# 代码中:

构造函数需要 ILogger、HttpClient 和 IMemoryCache。 _updateInterval 定义为 3 小时。 ExecuteAsync 方法: 在应用运行时循环。 向 "https://jsonplaceholder.typicode.com/photos" 发出 HTTP 请求,将响应映射为一组 Photo 对象。 照片组放置在 "Photos" 键下的 IMemoryCache 中。 _cacheSignal.Release() 被调用,释放任何正在等待信号的使用者。 根据更新间隔,等待对 Task.Delay 的调用。 延迟 3 小时后,缓存将再次更新。

同一进程中的使用者可以向 IMemoryCache 请求获取照片,但 CacheWorker 负责更新缓存。

分布式缓存

在某些场景中,需要使用分布式缓存,例如有多个应用服务器的情况。 分布式缓存支持比内存中缓存方法更广的横向扩展。 使用分布式缓存将缓存内存卸载到外部进程,但确实需要额外的网络 I/O 并会引入更多一点的延迟(即使是名义上)。

分布式缓存抽象是 Microsoft.Extensions.Caching.Memory NuGet 包的一部分,甚至还存在一个 AddDistributedMemoryCache 扩展方法。

注意

AddDistributedMemoryCache 只应在开发和/或测试场景中使用,它不是可行的生产实现。

请考虑以下包中 IDistributedCache 的任何可用实现:

Microsoft.Extensions.Caching.SqlServer Microsoft.Extensions.Caching.StackExchangeRedis NCache.Microsoft.Extensions.Caching.OpenSource 分布式缓存 API

分布式缓存 API 比对应的内存中缓存 API 更原始一些。 键值对更基本一些。 内存中缓存键基于 object,而分布式键基于 string。 对于内存中缓存,值可以是任何强类型的泛型,而分布式缓存中的值将保存为 byte[]。 这并不是说各种实现不会公开强类型的泛型值,而是公开实现的详细信息。

创建值

若要在分布式缓存中创建值,请调用其中一个 set API:

IDistributedCache.SetAsync IDistributedCache.Set

通过使用内存中缓存示例中的 AlphabetLetter 记录,你可以将对象串行化为 JSON,然后将 string 编码为 byte[]:

DistributedCacheEntryOptions options = new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration) }; AlphabetLetter alphabetLetter = new(letter); string json = JsonSerializer.Serialize(alphabetLetter); byte[] bytes = Encoding.UTF8.GetBytes(json); await cache.SetAsync(letter.ToString(), bytes, options);

与内存中缓存非常类似,缓存项可能有一些选项有助于微调它们在缓存中的存在,在本例中为 DistributedCacheEntryOptions。

创建扩展方法

有几种便利的扩展方法可用于创建值,这些方法有助于避免将对象的 string 表示形式编码为 byte[]:

DistributedCacheExtensions.SetStringAsync DistributedCacheExtensions.SetString 读取值

若要从分布式缓存读取值,请调用其中一个 Get API:

IDistributedCache.GetAsync IDistributedCache.Get AlphabetLetter? alphabetLetter = null; byte[]? bytes = await cache.GetAsync(letter.ToString()); if (bytes is { Length: > 0 }) { string json = Encoding.UTF8.GetString(bytes); alphabetLetter = JsonSerializer.Deserialize(json); }

从缓存读取缓存项后,可以从 string 中获取 UTF8 编码的 byte[] 表示形式

读取扩展方法

有几种便利的扩展方法可用于读取值,这些方法有助于避免将 byte[] 解码为对象的 string 表示形式:

DistributedCacheExtensions.GetStringAsync DistributedCacheExtensions.GetString 更新值

无法使用单个 API 调用更新分布式缓存中的值,但值可以使用其中一个 Refresh API 重置其可调过期:

IDistributedCache.RefreshAsync IDistributedCache.Refresh

如果需要更新实际值,则必须删除值,然后重新添加。

删除值

若要删除分布式缓存中的值,请调用其中一个 Remove API:

IDistributedCache.RemoveAsync IDistributedCache.Remove

提示

尽管存在上述 API 的同步版本,但请注意分布式缓存的实现依赖于网络 I/O。 因此,在更多情况下,更倾向于使用异步 API。

另请参阅 .NET 中的依赖关系注入 .NET 通用主机 .NET 中的辅助角色服务 面向 .NET 开发人员的 Azure ASP.NET Core 中的内存中缓存 ASP.NET Core 中的分布式缓存


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3