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

作者: Rick AndersonJohn LuoSteve Smith

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

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

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

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

使用 System.Runtime.Caching/MemoryCache

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

  • .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 天。 缓存条目在一天后从缓存中逐出,即使在超时期间访问该条目也是如此。

以下代码使用 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;
    });

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

GetOrCreateGetOrCreateAsyncGet 是类中的 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);
}

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

使用 MemoryCache.Compact 删除缓存项

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

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

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

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

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

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

以下示例显示了在依赖条目过期时如何使缓存条目过期。 缓存项中添加了一个CancellationChangeTokenCancel 方法在 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作用域内创建的缓存条目将会继承触发器和过期设置。

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

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

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

    没有计时器主动扫描缓存中的过期项目。 缓存上的任何活动(通过GetTryGetValueSetRemove)可以触发对过期项目的后台扫描。 在 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 已启用。 例如,在Program.cs文件中调用AddMvcAddControllersWithViewsAddRazorPagesAddMvcCore().AddRazorViewEngine和许多其他Add{Service}方法可启用IMemoryCache

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

使用后台缓存更新

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

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

缓存基础知识

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

ASP.NET Core 支持多种不同的缓存。 最简单的缓存基于 IMemoryCache. IMemoryCache 表示存储在 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(在本文中介绍),因为它可以更好地集成到 ASP.NET Core 中。 例如,IMemoryCache 自然支持 ASP.NET Core 依赖注入

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

缓存指南

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

使用 IMemoryCache

Warning

使用依赖项注入中的共享内存缓存并调用 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 extension 方法将数据缓存相对时间,而不创建 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 的值将保留在缓存中。

以下代码使用 GetOrCreate and GetOrCreateAsync 缓存数据。

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

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

GetOrCreateGetOrCreateAsyncGet 是类中的 CacheExtensions 扩展方法。 这些方法扩展了 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 上的 Compact 源代码

缓存依赖项

以下示例显示了在依赖条目过期时如何使缓存条目过期。 缓存项中添加了一个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);
}

使用 a 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} 方法之一的应用程序,可能需要调用 AddMemoryCacheConfigureServices.

后台缓存更新

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

其他资源