在 ASP.NET Core 中的内存中缓存

作者: Rick AndersonJohn LuoSteve Smith

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

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 或更高版本。

建议使用 System.Runtime.Caching/MemoryCacheMicrosoft.Extensions.Caching.Memory/IMemoryCache(在本文中介绍),因为它可以更好地集成到 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 限制缓存大小。 共享缓存是由其他框架或库共享的缓存。

内存缓存是使用 Dependency Injection 从应用程序引用的服务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 extension 方法将数据缓存相对时间,而无需 MemoryCacheEntryOptions

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

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

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

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

GetOrCreateGetOrCreateAsyncGet 是类中的 CacheExtensions 扩展方法。 这些方法扩展了 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 创建为独立内存高速缓存,供知道此大小受限高速缓存并知道如何适当设置高速缓存条目大小的组件使用。

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

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 源代码

缓存依赖项

以下示例显示了在依赖条目过期时如何使缓存条目过期。 A 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();
}

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

其他注释

  • 过期不会在后台进行。 没有计时器主动扫描缓存中的过期项目。 缓存 (GetTryGetValueSetRemove、 ) 上的任何活动都可以触发对过期项目的后台扫描。 ()CancelAfter 上的CancellationTokenSource计时器也会删除该条目并触发对过期项目的扫描。 以下示例用于 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、 、 AddControllersWithViewsAddRazorPagesAddMvcCore().AddRazorViewEngine和 中的 许多其他Add{Service}方法 Program.cs会启用 IMemoryCache。 对于未调用上述 Add{Service} 方法之一的应用程序,可能需要调用 AddMemoryCacheProgram.cs.

后台缓存更新

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

其他资源

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

缓存基础知识

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

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 或更高版本。

建议使用 System.Runtime.Caching/MemoryCacheMicrosoft.Extensions.Caching.Memory/IMemoryCache(在本文中介绍),因为它可以更好地集成到 ASP.NET Core 中。 例如, IMemoryCache 本机适用于 ASP.NET Core 依赖项注入

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

缓存准则

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

使用 IMemoryCache

警告

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

内存缓存是使用 Dependency Injection 从应用程序引用的服务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
  • 设置 a 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 运行时不会剪裁缓存。 应用程序的架构必须符合以下要求:

  • 限制缓存增长。
  • 调用 Compact 或在 Remove 可用内存有限时:

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

缓存条目的大小可以通过 或 SetSize 扩展方法设置Size

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 源代码

缓存依赖项

以下示例显示了在依赖条目过期时如何使缓存条目过期。 A CancellationChangeToken 将添加到缓存项中。 在 上调用 时 CancelCancellationTokenSource两个缓存条目都会被逐出。

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、 ) 上的任何活动都可以触发对过期项目的后台扫描。 ()CancelAfter 上的CancellationTokenSource计时器也会删除该条目并触发对过期项目的扫描。 以下示例用于 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 因为它仍被缓存条目使用。 传递给 MemoryCacheEntryOptions to CancellationToken 以创建在一定时间后过期的缓存条目。 因此 Dispose ,在缓存条目被删除或过期之前,不应调用。 示例代码调用该方法 RegisterPostEvictionCallback 以注册一个回调,该回调将在逐出缓存条目时调用,并在该回调中释放 CancellationTokenSource

  • 对于大多数应用程序, IMemoryCache 已启用。 例如,调用 AddMvc、 、 AddControllersWithViewsAddRazorPagesAddMvcCore().AddRazorViewEngine和 中的 许多其他Add{Service}方法 ConfigureServices会启用 IMemoryCache。 对于未调用上述 Add{Service} 方法之一的应用程序,可能需要调用 AddMemoryCacheConfigureServices.

后台缓存更新

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

其他资源