在 ASP.NET Core 中实现内存缓存

缓存可以通过减少生成内容所需的工作来显著提高应用程序的性能和可扩展性。 缓存最适用于不常更改且生成成本很高的数据。 缓存会创建比从源返回快得多的数据副本。 应用程序应该在编写和测试时永远 不 依赖于缓存的数据。

ASP.NET Core 支持多种不同的缓存。 最简单的缓存基于 IMemoryCache 接口。
IMemoryCache 表示存储在 Web 服务器内存中的缓存。 在服务器场(多个服务器)上运行的应用程序应确保在使用内存缓存时会话具有粘性。 粘性会话可确保来自客户端的所有请求都发送到同一服务器。 例如,Azure Web 应用使用 Microsoft 应用程序请求路由(ARR)将所有请求路由到同一服务器。

Web 场中的非粘性会话需要 分布式缓存 以避免缓存一致性问题。 对于某些应用程序,分布式缓存可以支持比内存中缓存更高的横向扩展。 使用分布式缓存会将缓存内存卸载到外部进程。

内存中缓存可以存储任何对象。 分布式缓存接口仅限于 byte[]。 内存中缓存和分布式缓存将缓存项存储为键值对。

使用 System.Runtime.Caching/MemoryCache

System.Runtime.Caching
/
MemoryCache (NuGet 包)可用于:

  • .NET Standard 2.0 或更高版本
  • 面向 .NET 标准 2.0 或更高版本的任何 .NET 实现(如 ASP.NET Core 3.1 或更高版本)
  • .NET Framework 4.5 或更高版本

Microsoft.Extensions.Caching.Memory/IMemoryCache(本文所述)建议优先于System.Runtime.Caching/MemoryCache,因为它提供了更好的与 ASP.NET Core 的集成。 例如,IMemoryCache 自然支持 ASP.NET Core 依赖注入。

在将代码从 ASP.NET 4.x 移植到 ASP.NET Core 时用作 System.Runtime.Caching/MemoryCache 兼容性桥梁。

审阅内存缓存指南

以下准则适用于内存中缓存:

  • 代码应始终具有回退选项来提取数据, 而不 依赖于缓存值的可用性。

  • 缓存使用内存,这是一个稀缺的资源。 限制缓存增长:

    • 不要 在缓存中插入外部输入。 例如,不建议使用任意用户提供的输入作为缓存键,因为输入可能会消耗不可预知的内存量。

    • 使用过期时间限制缓存增长。

    • 使用 SetSize、Size 和 SizeLimit 来限制缓存大小。 ASP.NET Core 运行时不根据内存压力限制缓存大小。 开发人员负责限制缓存大小。

创建 IMemoryCache 实例

内存缓存是一种服务,应用通过使用依赖项注入来引用。

Warning

如果多个框架或库使用相同的缓存,则它是 共享 缓存。 如果从依赖注入中使用共享内存缓存,同时使用、和来限制缓存大小,则应用可能会失败。

在缓存上设置大小限制时,所有条目都必须在添加时指定大小。 此方法可能会导致问题,因为开发人员可能无法完全控制使用共享缓存的内容。

若要使用 SetSize 方法、 Size 属性或 SizeLimit 属性限制缓存大小,请创建用于缓存的缓存单一实例。 有关更多信息和示例,请参阅 使用 SetSize、Size 和 SizeLimit 限制缓存大小。

在构造函数中请求IMemoryCache实例:

public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...

以下代码使用TryGetValue方法检查某个时间点是否在缓存中。 如果某个时间未缓存,则会使用 Set 方法创建一个新条目并将其添加到缓存中。

public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }

    CacheCurrentDateTime = cacheValue;
}

在前面的代码中,缓存项配置为滑动过期 3 秒。 如果未访问缓存条目超过 3 秒,则会从缓存中逐出该条目。 每次访问缓存条目时,它都会在缓存中再保留 3 秒。 该 CacheKeys 类是下载示例的一部分。

将显示当前时间和缓存时间:

<ul>
    <li>Current Time: @Model.CurrentDateTime</li>
    <li>Cached Time: @Model.CacheCurrentDateTime</li>
</ul>

下面的代码使用 Set 扩展方法在相对的时间周期内缓存数据,而无需 MemoryCacheEntryOptions:

_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));

在上面的代码中,缓存条目的相对过期时间为 1 天。 缓存条目在一天后从缓存中逐出,即使在超时期间访问该条目也是如此。

以下代码使用 GetOrCreate 和 GetOrCreateAsync 方法缓存数据。

public void OnGetCacheGetOrCreate()
{
    var cachedValue = _memoryCache.GetOrCreate(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return DateTime.Now;
        });

    // ...
}

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}

以下代码调用 Get 该方法提取缓存的时间:

var cacheEntry = _memoryCache.Get<DateTime?>(CacheKeys.Entry);

以下代码获取或创建具有绝对过期时间的缓存项:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.Entry,
    cacheEntry =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

仅具有可滑动过期时间的缓存项集存在永不过期的风险。 如果在滑动过期间隔内重复访问缓存的项目,则该项目永远不会过期。 将滑动过期与绝对过期相结合可确保项目过期。 绝对过期设置可缓存项的时长上限。 如果在滑动过期间隔内没有请求该项,系统仍然允许该项提前过期。 如果滑动过期间隔 或 绝对过期时间通过,则会从缓存中逐出该项。

以下代码获取或创建具有滑动和绝对到期时间的缓存项:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.CallbackEntry,
    cacheEntry =>
    {
        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

上述代码保证数据缓存的时间不会超过绝对时间。

GetOrCreate、 GetOrCreateAsync和 Get 是类中的 CacheExtensions 扩展方法。 这些方法扩展了 IMemoryCache的功能。

为条目创建 MemoryCacheEntryOptions

以下示例演示如何为条目创建 MemoryCacheEntryOptions 。 该代码完成以下任务:

public void OnGetCacheRegisterPostEvictionCallback()
{
    var memoryCacheEntryOptions = new MemoryCacheEntryOptions()
        .SetPriority(CacheItemPriority.NeverRemove)
        .RegisterPostEvictionCallback(PostEvictionCallback, _memoryCache);

    _memoryCache.Set(CacheKeys.CallbackEntry, DateTime.Now, memoryCacheEntryOptions);
}

private static void PostEvictionCallback(
    object cacheKey, object cacheValue, EvictionReason evictionReason, object state)
{
    var memoryCache = (IMemoryCache)state;

    memoryCache.Set(
        CacheKeys.CallbackMessage,
        $"Entry {cacheKey} was evicted: {evictionReason}.");
}

使用 SetSize、Size 和 SizeLimit 限制缓存大小

MemoryCache实例可以选择指定并强制实施大小限制。 缓存大小限制没有定义的度量单位,因为缓存没有测量条目大小的机制。 如果设置了高速缓存大小限制,则所有条目都必须指定大小。 ASP.NET Core 运行时不会根据内存压力限制缓存大小。 由开发人员来限制缓存大小。 指定的大小以开发人员选择的单位为单位。

例如:

  • 如果 Web 应用主要缓存字符串,则每个缓存条目大小可能是字符串长度。
  • 应用可以将所有条目的大小指定为 1,大小限制是条目计数。

如果未设置该 SizeLimit 属性,缓存将会无限增长。 当系统内存较低时,ASP.NET Core 运行时不会剪裁缓存。 应用程序的架构必须符合以下要求:

  • 限制缓存增长。
  • 当可用内存受限时,调用Compact方法或Remove方法。

以下代码创建一个可通过MemoryCache访问的无单元固定大小实例:

public class MyMemoryCache
{
    public MemoryCache Cache { get; } = new MemoryCache(
        new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
}

该 SizeLimit 属性没有单位。 缓存项在设置缓存大小限制时,必须以认为最合适的单位来指定大小。 缓存实例的所有用户都应使用相同的单位系统。 如果缓存的项大小之和超过指定的 SizeLimit值,则不会缓存条目。 如果未设置缓存大小限制,则忽略在条目上设置的缓存大小。

以下代码将 MyMemoryCache 实例注册到 依赖项注入 容器:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSingleton<MyMemoryCache>();

MyMemoryCache 被创建为独立内存缓存,供知晓此大小受限缓存并能适当设置缓存条目大小的组件使用。

可以使用扩展方法或SetSize属性设置Size缓存条目的大小:

if (!_myMemoryCache.Cache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSize(1);

    // cacheEntryOptions.Size = 1;

    _myMemoryCache.Cache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
}

在上面的代码中,突出显示的两行实现了设置缓存条目大小的相同结果。 在将调用链接到 SetSize 上时,将为方便起见提供 new MemoryCacheOptions() 方法。

使用 MemoryCache.Compact 删除缓存项

该方法 MemoryCache.Compact 尝试按以下顺序删除缓存的指定百分比:

  • 所有过期项目
  • 按优先级排序的项,其中首先删除了最低优先级项
  • 最近使用最少的对象
  • 具有最早绝对到期时间的项目
  • 具有最早滑动过期时间的项目

具有优先级 NeverRemove 的固定项 永远不会被删除。 以下代码删除缓存项并调用 Compact 该方法以删除缓存项的 25 个%:

_myMemoryCache.Cache.Remove(CacheKeys.Entry);
_myMemoryCache.Cache.Compact(.25);

有关更多信息,请参阅 GitHub 上的 Compact 源代码。

逐出具有过期依赖项的缓存条目

以下示例显示了在依赖条目过期时如何使缓存条目过期。 缓存项中添加了一个CancellationChangeToken。
Cancel 方法在 CancellationTokenSource 对象上被调用时,将清除两个缓存条目:

public void OnGetCacheCreateDependent()
{
    var cancellationTokenSource = new CancellationTokenSource();

    _memoryCache.Set(
        CacheKeys.DependentCancellationTokenSource,
        cancellationTokenSource);

    using var parentCacheEntry = _memoryCache.CreateEntry(CacheKeys.Parent);

    parentCacheEntry.Value = DateTime.Now;

    _memoryCache.Set(
        CacheKeys.Child,
        DateTime.Now,
        new CancellationChangeToken(cancellationTokenSource.Token));
}

public void OnGetCacheRemoveDependent()
{
    var cancellationTokenSource = _memoryCache.Get<CancellationTokenSource>(
        CacheKeys.DependentCancellationTokenSource);

    cancellationTokenSource.Cancel();
}

使用 CancellationTokenSource 对象时,可以将多个缓存条目逐出为一个组。 在前述代码中的using模式下,using作用域内创建的缓存条目将会继承触发器和过期设置。

查看有关内存中缓存的说明

以下说明适用于内存中缓存:

  • 到期不会在后台中发生。

    没有计时器主动扫描缓存中的过期项目。 缓存上的任何活动(通过Get、TryGetValue或SetRemove)可以触发对过期项目的后台扫描。 在 CancellationTokenSource 对象上使用 CancelAfter 方法设置的计时器也会删除条目并触发对过期项的扫描。

    以下示例使用 CancellationTokenSource(TimeSpan) 的重载构造函数来处理已注册的令牌。 当此标记触发时,它会立即删除该条目并触发淘汰回调:

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = DateTime.Now;
    
        var cancellationTokenSource = new CancellationTokenSource(
            TimeSpan.FromSeconds(10));
    
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(
                new CancellationChangeToken(cancellationTokenSource.Token))
            .RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                ((CancellationTokenSource)state).Dispose();
            }, cancellationTokenSource);
    
        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }
    
  • 使用回调重新填充缓存项时:

    • 多个请求可能会发现缓存的键值为空,因为回调未完成。
    • 此方法可能会导致多个线程重新填充缓存的项。
  • 当一个缓存项(父项)创建另一个条目(子项)时,子项将复制父项的过期令牌和基于时间的过期设置。 子项不会因手动删除或更新父条目而过期。

  • 使用 PostEvictionCallbacks 属性指定在从缓存中逐出缓存条目后应触发的回调。

  • 对于大多数应用程序, IMemoryCache 已启用。 例如,在AddMvc文件中调用AddControllersWithViews、AddRazorPages、AddMvcCore().AddRazorViewEngine、Add{Service}和许多其他方法可启用IMemoryCache。

    对于不调用上述Add{Service}方法之一的应用,可能需要调用AddMemoryCacheProgram.cs文件中的方法。

使用后台缓存更新

使用 后台服务 (如 IHostedService 接口)更新缓存。 后台服务可以重新计算这些条目,并仅在它们准备就绪后将其分配给缓存。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容