Caché en memoria en ASP.NET Core

Por Rick Anderson, John Luoy Steve Smith

Vea o descargue el código de ejemplo (cómo descargarlo)

Conceptos básicos del almacenamiento en caché

El almacenamiento en caché puede mejorar significativamente el rendimiento y la escalabilidad de una aplicación al reducir el trabajo necesario para generar contenido. El almacenamiento en caché funciona mejor con datos que cambian con poca frecuencia y que son costosos de generar. El almacenamiento en caché hace una copia de los datos que se pueden devolver mucho más rápido que desde el origen. Las aplicaciones deben escribirse y probarse para que nunca dependan de los datos almacenados en caché.

ASP.NET Core admite varias cachés diferentes. La caché más sencilla se basa en IMemoryCache. IMemoryCache representa una memoria caché almacenada en la memoria del servidor web. Las aplicaciones que se ejecutan en una granja de servidores (varios servidores) deben garantizar que las sesiones sean permanentes cuando se usa la memoria caché en memoria. Las sesiones permanentes garantizan que las solicitudes posteriores de un cliente vayan al mismo servidor. Por ejemplo, las aplicaciones web de Azure usan el enrutamiento de solicitudes de aplicación (ARR) para enrutar todas las solicitudes posteriores al mismo servidor.

Las sesiones no permanentes de una granja de servidores web requieren una caché distribuida para evitar problemas de coherencia de caché. Para algunas aplicaciones, una caché distribuida puede admitir un escalado horizontal mayor que una caché en memoria. El uso de una caché distribuida descarga la memoria caché en un proceso externo.

La memoria caché en memoria puede almacenar cualquier objeto. La interfaz de caché distribuida se limita a byte[] . Los elementos de caché en memoria y caché distribuida almacenan elementos como pares clave-valor.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache(NuGet paquete) se puede usar con:

  • .NET Standard 2.0 o posterior.
  • Cualquier implementación de .NET que se .NET Standard 2.0 o posterior. Por ejemplo, ASP.NET Core 3.1 o posterior.
  • .NET Framework 4.5 o superior.

Se recomienda Microsoft.Extensions.Caching.Memory (descrito en este artículo) porque está mejor integrado en / IMemoryCache System.Runtime.Caching / MemoryCache ASP.NET Core. Por ejemplo, IMemoryCache funciona de forma nativa con ASP.NET Core inserción de dependencias.

Use System.Runtime.Caching / MemoryCache como puente de compatibilidad al portear código de ASP.NET 4.x a ASP.NET Core.

Directrices de caché

  • El código siempre debe tener una opción de reserva para capturar datos y no depender de que haya disponible un valor almacenado en caché.
  • La memoria caché usa un recurso insuficiente, memoria. Limitar el crecimiento de la memoria caché:
    • No use la entrada externa como claves de caché.
    • Use expiraciones para limitar el crecimiento de la memoria caché.
    • Use SetSize, Size y SizeLimit para limitar el tamaño de caché. El entorno ASP.NET Core tiempo de ejecución no limita el tamaño de caché en función de la presión de memoria. El desarrollador debe limitar el tamaño de la memoria caché.

Uso de IMemoryCache

Advertencia

El uso de una memoria caché compartida desde la inserción de dependencias y la llamada a , o para limitar el tamaño de la memoria caché puede provocar un error en la SetSize Size SizeLimit aplicación. Cuando se establece un límite de tamaño en una memoria caché, todas las entradas deben especificar un tamaño al agregarse. Esto puede provocar problemas, ya que es posible que los desarrolladores no tengan control total sobre lo que usa la memoria caché compartida. Al usar SetSize , o para limitar la Size SizeLimit caché, cree un singleton de caché para el almacenamiento en caché. Para obtener más información y un ejemplo, vea Usar SetSize, Size y SizeLimit para limitar el tamaño de caché. Una caché compartida es una que comparten otros marcos o bibliotecas.

El almacenamiento en caché en memoria es un servicio al que se hace referencia desde una aplicación mediante la inserción de dependencias. Solicite la IMemoryCache instancia en el constructor:

public class HomeController : Controller
{
    private IMemoryCache _cache;

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

El código siguiente usa TryGetValue para comprobar si hay una hora en la memoria caché. Si no se almacena una hora en caché, se crea una nueva entrada y se agrega a la memoria caché con Set. La CacheKeys clase forma parte del ejemplo de descarga.

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

Se muestran la hora actual y la hora almacenada en caché:

@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>

El código siguiente usa el método de extensión Set para almacenar en caché los datos durante un tiempo relativo sin crear el MemoryCacheEntryOptions objeto .

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

El valor DateTime almacenado en caché permanece en la memoria caché mientras hay solicitudes dentro del período de tiempo de espera.

El código siguiente usa GetOrCreate y GetOrCreateAsync para almacenar en caché los datos.

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

El código siguiente llama a Get para capturar la hora almacenada en caché:

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

El código siguiente obtiene o crea un elemento almacenado en caché con expiración absoluta:

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

    return View("Cache", cacheEntry);
}

Un conjunto de elementos almacenados en caché con una expiración deslizante solo corre el riesgo de quedar obsoleto. Si se accede a él con más frecuencia que el intervalo de expiración deslizante, el elemento nunca expirará. Combine una expiración deslizante con una expiración absoluta para garantizar que el elemento expira una vez transcurrido el tiempo de expiración absoluto. La expiración absoluta establece un límite superior en cuanto a cuánto tiempo se puede almacenar en caché el elemento, a la vez que permite que expire antes si no se solicita dentro del intervalo de expiración deslizante. Cuando se especifica una expiración absoluta y deslizante, las expiraciones son lógicamente ORed. Si se supera el intervalo de expiración deslizante o el tiempo de expiración absoluto, el elemento se expulsa de la memoria caché.

El código siguiente obtiene o crea un elemento almacenado en caché con una expiración deslizante y absoluta:

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

El código anterior garantiza que los datos no se almacenarán en caché más tiempo que el tiempo absoluto.

GetOrCreate, GetOrCreateAsync y son Get métodos de extensión de la clase CacheExtensions . Estos métodos amplían la funcionalidad de IMemoryCache .

MemoryCacheEntryOptions

El ejemplo siguiente:

  • Establece una hora de expiración deslizante. Las solicitudes que tienen acceso a este elemento almacenado en caché restablecerán el reloj de expiración deslizante.
  • Establece la prioridad de caché en CacheItemPriority.NeverRemove.
  • Establece un PostEvictionDelegate al que se llamará después de que la entrada se expulse de la memoria caché. La devolución de llamada se ejecuta en un subproceso diferente del código que quita el elemento de la memoria caché.
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);
}

Uso de SetSize, Size y SizeLimit para limitar el tamaño de caché

MemoryCacheOpcionalmente, una instancia puede especificar y aplicar un límite de tamaño. El límite de tamaño de caché no tiene una unidad de medida definida porque la memoria caché no tiene ningún mecanismo para medir el tamaño de las entradas. Si se establece el límite de tamaño de caché, todas las entradas deben especificar el tamaño. El entorno ASP.NET Core tiempo de ejecución no limita el tamaño de caché en función de la presión de memoria. El desarrollador debe limitar el tamaño de la memoria caché. El tamaño especificado se encuentra en las unidades que elige el desarrollador.

Por ejemplo:

  • Si la aplicación web almacenaba principalmente cadenas en caché, cada tamaño de entrada de caché podría ser la longitud de la cadena.
  • La aplicación podría especificar el tamaño de todas las entradas como 1 y el límite de tamaño es el recuento de entradas.

Si SizeLimit no se establece, la memoria caché crece sin límite. El ASP.NET Core de ejecución no recorta la memoria caché cuando la memoria del sistema es baja. Las aplicaciones deben diseñarse para:

  • Limitar el crecimiento de la memoria caché.
  • Llame Compact a o cuando la memoria disponible esté Remove limitada:

El código siguiente crea un tamaño fijo sin unidad accesible MemoryCache mediante la inserción de dependencias:

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

SizeLimit no tiene unidades. Las entradas almacenadas en caché deben especificar el tamaño en las unidades que consideren más adecuadas si se ha establecido el límite de tamaño de caché. Todos los usuarios de una instancia de caché deben usar el mismo sistema de unidades. Una entrada no se almacenará en caché si la suma de los tamaños de entrada almacenados en caché supera el valor especificado por SizeLimit . Si no se establece ningún límite de tamaño de caché, se omitirá el tamaño de caché establecido en la entrada.

El código siguiente se registra MyMemoryCache con el contenedor de inserción de dependencias.

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

MyMemoryCache se crea como una caché de memoria independiente para los componentes que son conscientes de este tamaño de caché limitada y saben cómo establecer el tamaño de entrada de la memoria caché adecuadamente.

En el código siguiente se usa 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");
    }
}

El tamaño de la entrada de caché se puede establecer mediante Size o los SetSize métodos de extensión:

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 intenta quitar el porcentaje especificado de la memoria caché en el orden siguiente:

  • Todos los elementos expirados.
  • Elementos por prioridad. Los elementos de prioridad más baja se quitan primero.
  • Objetos usados menos recientemente.
  • Elementos con la expiración absoluta más temprana.
  • Elementos con la expiración deslizante más temprana.

Los elementos anclados con prioridad NeverRemove nunca se quitan. El código siguiente quita un elemento de caché y llama a Compact :

_cache.Remove(MyKey);

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

Consulte Origen de Compact en GitHub para obtener más información.

Dependencias de caché

En el ejemplo siguiente se muestra cómo expirar una entrada de caché si expira una entrada dependiente. Se CancellationChangeToken agrega al elemento almacenado en caché. Cuando Cancel se llama a en , se CancellationTokenSource expulsa ambas entradas de caché.

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

El uso de permite expulsar varias entradas de caché CancellationTokenSource como un grupo. Con el patrón en el código anterior, las entradas de caché creadas dentro del using using bloque heredarán los desencadenadores y la configuración de expiración.

Notas adicionales

  • La expiración no se da en segundo plano. No hay ningún temporizador que examina activamente la memoria caché en busca de elementos expirados. Cualquier actividad de la caché ( Get , , ) puede desencadenar un examen en segundo plano de los elementos Set Remove expirados. Un temporizador en CancellationTokenSource ( ) también quita la entrada y desencadena un examen de elementos CancelAfter expirados. En el ejemplo siguiente se usa CancellationTokenSource(TimeSpan) para el token registrado. Cuando este token se desvía, se quita la entrada inmediatamente y se inician las devoluciones de llamada de expulsión:
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);
}
  • Cuando se usa una devolución de llamada para volver a llenar un elemento de caché:

    • Varias solicitudes pueden encontrar el valor de clave almacenado en caché vacío porque la devolución de llamada no se ha completado.
    • Esto puede dar lugar a que varios subprocesos repopulten el elemento almacenado en caché.
  • Cuando se usa una entrada de caché para crear otra, el elemento secundario copia los tokens de expiración de la entrada primaria y la configuración de expiración basada en el tiempo. El elemento secundario no ha expirado mediante la eliminación manual o la actualización de la entrada primaria.

  • Use para establecer las devoluciones de llamada que se desencadenan después de que se expulse la entrada de caché PostEvictionCallbacks de la memoria caché.

  • Para la mayoría de las aplicaciones, IMemoryCache está habilitado. Por ejemplo, llamar a , , , y muchos otros AddMvc AddControllersWithViews AddRazorPages AddMvcCore().AddRazorViewEngine Add{Service} métodos en ConfigureServices , habilita IMemoryCache . Para las aplicaciones que no llaman a uno de los métodos anteriores, puede ser necesario Add{Service} llamar AddMemoryCache a en ConfigureServices .

Actualización de caché en segundo plano

Use un servicio en segundo plano como para actualizar la memoria IHostedService caché. El servicio en segundo plano puede volver a compilar las entradas y, a continuación, asignarlas a la memoria caché solo cuando estén listas.

Recursos adicionales