ASP.NET Core 中的内存中缓存

作者:Rick AndersonJohn LuoSteve Smith

缓存可以通过减少生成内容所需的工作,显著提高应用的性能和可伸缩性。 缓存最适用于不常更改且生成成本很高的数据。 缓存生成的数据副本可以比从源中更快地返回。 应该以从不依赖于缓存数据的方式编写和测试应用

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

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

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

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCacheNuGet 包)可用于:

  • .NET Standard 2.0 或更高版本。
  • 面向 .NET Standard 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

警告

如果使用依赖关系注入中的共享内存缓存,并调用 SetSizeSizeSizeLimit 来限制缓存大小,则可能会导致应用失败。 在缓存上设置大小限制时,所有条目在添加时都必须指定大小。 这可能会导致问题,因为开发人员可能无法完全控制使用共享缓存的内容。 当使用 SetSizeSizeSizeLimit 来限制缓存时,为缓存创建一个缓存单一实例。 有关详细信息和示例,请参阅使用 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 秒。 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));

在上面的代码中,为缓存条目配置了一天的相对到期时间。 缓存条目在一天之后从缓存中逐出,即使在此超时期内访问该项也是如此。

以下代码使用 GetOrCreateGetOrCreateAsync 来缓存数据。

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;
    });

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

GetOrCreateGetOrCreateAsyncGetCacheExtensions 类中的扩展方法。 这些方法扩展了 IMemoryCache 的功能。

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 运行时不会剪裁缓存。 应用必须构建为:

  • 限制缓存增长。
  • 在可用内存受限时调用 CompactRemove

以下代码创建了一个可通过依赖关系注入访问的无单位固定大小 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);
}

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

MemoryCache.Compact

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

  • 所有到期项。
  • 按优先级排列的项。 首先删除最低优先级的项。
  • 最近最少使用的对象。
  • 绝对到期时间最短的项。
  • 可调到期时间最短的项。

永远不会删除优先级为 NeverRemove 的固定项。 以下代码将删除缓存项,并调用 Compact 以删除 25% 的缓存条目:

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

有关详细信息,请参阅 GitHub 上的压缩源

缓存依赖项

下面的示例演示如何在相关条目到期时使缓存条目过期。 CancellationChangeToken 已添加到缓存项。 在 CancellationTokenSource 上调用 Cancel 时,两个缓存条目都将被逐出:

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 范围内创建的缓存条目会继承触发器和到期设置。

附加说明

  • 到期不会在后台中发生。 没有计时器可以主动扫描缓存中的到期项。 缓存上的任何活动(GetSetRemove)都可触发在后台扫描到期项。 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。 例如,调用 AddMvcAddControllersWithViewsAddRazorPagesAddMvcCore().AddRazorViewEngineProgram.cs 中的许多其他 Add{Service} 方法可以启用 IMemoryCache。 对于不调用上述 Add{Service} 方法之一的应用,可能需要调用 Program.cs 中的 AddMemoryCache

后台缓存更新

使用后台服务(如 IHostedService)更新缓存。 后台服务可重新计算条目,然后仅在其准备就绪时将其分配给缓存。

其他资源

查看或下载示例代码如何下载

缓存基础知识

缓存可以通过减少生成内容所需的工作,显著提高应用的性能和可伸缩性。 缓存最适用于不常更改且生成成本很高的数据。 缓存生成的数据副本可以比从源中更快地返回。 应该以从不依赖于缓存数据的方式编写和测试应用

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

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

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

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCacheNuGet 包)可用于:

  • .NET Standard 2.0 或更高版本。
  • 面向 .NET Standard 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 作为兼容性桥。

缓存指南

  • 代码应始终具有回退选项,以提取数据,而不依赖于可用的缓存值
  • 缓存使用短缺资源:内存。 限制缓存增长:

使用 IMemoryCache

警告

如果使用依赖关系注入中的共享内存缓存,并调用 SetSizeSizeSizeLimit 来限制缓存大小,则可能会导致应用失败。 在缓存上设置大小限制时,所有条目在添加时都必须指定大小。 这可能会导致问题,因为开发人员可能无法完全控制使用共享缓存的内容。 当使用 SetSizeSizeSizeLimit 来限制缓存时,为缓存创建一个缓存单一实例。 有关详细信息和示例,请参阅使用 SetSize、Size 和 SizeLimit 限制缓存大小。 共享缓存是由其他框架或库共享的缓存。

内存中缓存是一项使用依赖关系注入从应用引用的服务。 在构造函数中请求 IMemoryCache 实例:

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

下面的代码使用 TryGetValue 来检查缓存中是否包含时间。 如果未缓存时间,则创建一个新条目,并使用 Set 将该条目添加到缓存中。 CacheKeys 类是下载示例的一部分。

public static class CacheKeys
{
    public static string Entry => "_Entry";
    public static string CallbackEntry => "_Callback";
    public static string CallbackMessage => "_CallbackMessage";
    public static string Parent => "_Parent";
    public static string Child => "_Child";
    public static string DependentMessage => "_DependentMessage";
    public static string DependentCTS => "_DependentCTS";
    public static string Ticks => "_Ticks";
    public static string CancelMsg => "_CancelMsg";
    public static string CancelTokenSource => "_CancelTokenSource";
}
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

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

@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsynchronous">CacheGetOrCreateAsynchronous</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbs">CacheGetOrCreateAbs</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbsSliding">CacheGetOrCreateAbsSliding</a></li>

    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

以下代码使用 Set 扩展方法,在不创建 MemoryCacheEntryOptions 对象的情况下,将数据缓存相对长的一段时间:

public IActionResult SetCacheRelativeExpiration()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Save data in cache and set the relative expiration time to one day
        _cache.Set(CacheKeys.Entry, cacheEntry, TimeSpan.FromDays(1));
    }

    return View("Cache", cacheEntry);
}

当超时期限内有请求时,缓存的 DateTime 值保留在缓存中。

以下代码使用 GetOrCreateGetOrCreateAsync 来缓存数据。

public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsynchronous()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    return View("Cache", cacheEntry);
}

以下代码调用 Get 来提取缓存的数据:

public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

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

public IActionResult CacheGetOrCreateAbs()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

仅具有可调到期时间的缓存项集存在永不过期的风险。 如果在可调到期时间间隔内重复访问缓存的项,则该项永远不会过期。 将可调到期时间与绝对到期时间结合以确保该项过期。 绝对到期时间将上线设置为项的可缓存时间,同时如果在可调到期时间内未请求项目,仍允许项提前到期。 如果经过了可调到期间隔或绝对到期时间,则会从缓存中逐出项

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

public IActionResult CacheGetOrCreateAbsSliding()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

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

GetOrCreateGetOrCreateAsyncGetCacheExtensions 类中的扩展方法。 这些方法扩展了 IMemoryCache 的功能。

MemoryCacheEntryOptions

以下示例:

  • 设置可调到期时间。 访问此缓存项的请求将重置可调到期时钟。
  • 将缓存优先级设置为 CacheItemPriority.NeverRemove
  • 设置将在从缓存中逐出条目后调用的 PostEvictionDelegate。 回调在与从缓存中删除项的代码不同的线程上运行。
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

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

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

例如:

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

如果未设置 SizeLimit,则缓存会无限增长。 系统内存不足时,ASP.NET Core 运行时不会剪裁缓存。 应用必须构建为:

  • 限制缓存增长。
  • 在可用内存受限时调用 CompactRemove

以下代码创建了一个可通过依赖关系注入访问的无单位固定大小 MemoryCache

// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; private set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}

SizeLimit 没有单位。 如果已设置缓存大小限制,则缓存条目必须以其认为最适合的任何单位指定大小。 缓存实例的所有用户都应使用相同的单位系统。 如果缓存的条目大小之和超出了 SizeLimit 指定的值,则不会缓存条目。 如果未设置缓存大小限制,则将忽略条目上设置的缓存大小。

以下代码使用依赖关系注入容器注册 MyMemoryCache

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache 被创建为用于组件的独立内存缓存,这些组件已知晓这种大小受限的缓存并知道如何相应地设置缓存条目大小。

以下代码使用 MyMemoryCache

public class SetSize : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public SetSize(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // Set cache entry size by extension method.
                .SetSize(1)
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}

可以通过 SizeSetSize 扩展方法设置缓存条目的大小:

public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Set cache entry size by extension method.
            .SetSize(1)
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}

MemoryCache.Compact

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

  • 所有到期项。
  • 按优先级排列的项。 首先删除最低优先级的项。
  • 最近最少使用的对象。
  • 绝对到期时间最短的项。
  • 可调到期时间最短的项。

永远不会删除优先级为 NeverRemove 的固定项。 以下代码将删除缓存项,并调用 Compact

_cache.Remove(MyKey);

// Remove 33% of cached items.
_cache.Compact(.33);   
cache_size = _cache.Count;

有关详细信息,请参阅 GitHub 上的压缩源

缓存依赖项

下面的示例演示如何在相关条目到期时使缓存条目过期。 CancellationChangeToken 已添加到缓存项。 在 CancellationTokenSource 上调用 Cancel 时,两个缓存条目都将被逐出。

public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

若使用 CancellationTokenSource,则允许将多个缓存条目作为一个组逐出。 使用上述代码中的 using 模式,在 using 块内创建的缓存条目将继承触发器和到期设置。

其他注释

  • 到期不会在后台中发生。 没有计时器可以主动扫描缓存中的到期项。 缓存上的任何活动(GetSetRemove)都可触发在后台扫描到期项。 CancellationTokenSource 上的计时器 (CancelAfter) 也会删除条目并触发扫描到期项。 下面的示例将 CancellationTokenSource(TimeSpan) 用于已注册的令牌。 当此令牌触发时,它会立即删除条目,并引发逐出回调:

    public IActionResult CacheAutoExpiringTryGetValueSet()
    {
        DateTime cacheEntry;
    
        if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
        {
            cacheEntry = DateTime.Now;
    
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .AddExpirationToken(new CancellationChangeToken(cts.Token));
    
            _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
        }
    
        return View("Cache", cacheEntry);
    }
    
  • 使用回调重新填充缓存项时:

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

  • 使用 PostEvictionCallbacks 设置从缓存中逐出缓存条目后将触发的回调。 在示例代码中,调用了 CancellationTokenSource.Dispose() 来释放由 CancellationTokenSource 使用的非托管资源。 但是,CancellationTokenSource 不会立即被释放,因为缓存条目仍然在使用它。 CancellationToken 被传递给 MemoryCacheEntryOptions 来创建一个缓存条目,该条目将在某个时间后过期。 因此,在缓存项被移除或过期之前,不应调用 Dispose。 示例代码调用 RegisterPostEvictionCallback 方法来注册当缓存条目被逐出时将调用的回调,并在该回调中释放 CancellationTokenSource

  • 对于大多数应用,已启用 IMemoryCache。 例如,调用 AddMvcAddControllersWithViewsAddRazorPagesAddMvcCore().AddRazorViewEngineConfigureServices 中的许多其他 Add{Service} 方法可以启用 IMemoryCache。 对于不调用上述 Add{Service} 方法之一的应用,可能需要调用 ConfigureServices 中的 AddMemoryCache

后台缓存更新

使用后台服务(如 IHostedService)更新缓存。 后台服务可重新计算条目,然后仅在其准备就绪时将其分配给缓存。

其他资源