Compartir a través de


Caché en memoria en ASP.NET Core

Por Rick Anderson, John Luo y Steve Smith

El almacenamiento en caché puede mejorar significativamente el rendimiento y la escalabilidad de una aplicación reduciendo el trabajo necesario para generar contenido. El almacenamiento en caché funciona mejor con los datos que cambian con poca frecuencia y es costoso 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 memoria caché más sencilla se basa en .IMemoryCache IMemoryCache representa una 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 persistentes al usar la caché en memoria. Las sesiones permanentes garantizan que las solicitudes 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 al mismo servidor.

Las sesiones no permanentes de una granja de servidores web requieren una caché distribuida para evitar problemas de coherencia de caché. En 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 caché en memoria puede almacenar cualquier objeto. La interfaz de caché distribuida está limitada a byte[]. El caché en memoria y el caché distribuido almacenan elementos como pares clave-valor.

System.Runtime.Caching/MemoryCache

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

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

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

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

Directrices de almacenamiento en 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 caché usa un recurso escaso, la memoria. Limitar el crecimiento de la memoria caché:
    • No inserte entradas externas en la memoria caché. Por ejemplo, no se recomienda usar entradas arbitrarias proporcionadas por el usuario como clave de caché, ya que la entrada podría consumir una cantidad impredecible de memoria.
    • Use expiraciones para limitar el crecimiento de la memoria caché.
    • Utilice SetSize, Size y SizeLimit para limitar el tamaño de la caché. El entorno de ejecución de ASP.NET Core no limita el tamaño de caché en función de la presión de memoria. El desarrollador tiene que limitar el tamaño de la memoria caché.

Uso de IMemoryCache

Advertencia

El uso de una caché de memoria compartida de la inserción de dependencias y realizar llamadas a SetSize, Size o SizeLimit para limitar el tamaño de la caché puede hacer que la aplicación falle. 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 caché compartida. Al usar SetSize, Sizeo SizeLimit para limitar la memoria 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 la caché. Una caché compartida es una compartida por 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 IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

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

    // ...

El código siguiente usa TryGetValue para comprobar si hay una hora en la memoria caché. Si una hora no está en la caché, se crea una nueva entrada y se añade a la caché con 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;
}

En el código anterior, la entrada de caché se configura con una expiración deslizante de tres segundos. Si no se tiene acceso a la entrada de caché durante más de tres segundos, se expulsa de la memoria caché. Cada vez que se accede a la entrada de caché, permanece en la memoria caché durante más de 3 segundos. La CacheKeys clase forma parte del ejemplo de descarga.

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

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

El siguiente código utiliza el método de extensión Set para almacenar en caché los datos durante un tiempo relativo sin utilizar MemoryCacheEntryOptions:

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

En el código anterior, la entrada de caché se configura con una expiración relativa de un día. La entrada de caché se expulsa de la memoria caché después de un día, incluso si se accede a ella dentro de este período de tiempo de espera.

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

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

    // ...
}

El código siguiente llama Get a para capturar el tiempo almacenado en caché:

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

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

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

Un conjunto de elementos almacenados en caché con solo una expiración deslizante está en riesgo de que nunca expire. Si se accede repetidamente al elemento almacenado en caché dentro del intervalo de expiración deslizante, el elemento nunca expira. Combine una expiración deslizante con una expiración absoluta para garantizar que el elemento expira. La expiración absoluta establece un límite superior durante cuánto tiempo se puede almacenar en caché el elemento mientras se permite que el elemento expire antes si no se solicita dentro del intervalo de expiración deslizante. Si el intervalo de caducidad o el tiempo de caducidad absoluta pasan, el elemento se expulsa de la caché.

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

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

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

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

MemoryCacheEntryOptions

En el ejemplo siguiente:

  • Establece la prioridad de caché en CacheItemPriority.NeverRemove.
  • Establece un PostEvictionDelegate que es llamado después de que la entrada es desalojada de la caché. La devolución de llamada se ejecuta en un subproceso diferente del código que elimina el elemento de la caché.
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}.");
}

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

Opcionalmente, una MemoryCache 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 de ejecución de ASP.NET Core no limita el tamaño de caché en función de la presión de memoria. El desarrollador tiene que limitar el tamaño de la memoria caché. El tamaño especificado está en unidades que el desarrollador elige.

Por ejemplo:

  • Si la aplicación web estuviera almacenando en caché principalmente la cadena, cada entrada de caché podría tener como tamaño la longitud de 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 entorno de ejecución de ASP.NET Core 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 a Compact o Remove cuando la memoria disponible esté limitada.

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

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

SizeLimit no tiene unidades. Las entradas almacenadas en caché deben especificar el tamaño en las unidades que considere 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 omite el tamaño de caché establecido en la entrada.

El siguiente código registra MyMemoryCache con el contenedor de inyección de dependencia:

var builder = WebApplication.CreateBuilder(args);

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

MyMemoryCache se crea como una caché de memoria independiente para los componentes que tienen en cuenta este tamaño limitado y saben cómo establecer el tamaño de entrada de caché de forma adecuada.

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

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

En el código anterior, las dos líneas resaltadas logran el mismo resultado de establecer el tamaño de la entrada de caché. SetSize se proporciona para mayor comodidad al encadenar llamadas a new MemoryCacheOptions().

MemoryCache.Compact

MemoryCache.Compact intenta quitar el porcentaje especificado de la memoria caché en el orden siguiente:

  • Todos los artículos caducados.
  • 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.
  • Objetos con la fecha de caducidad más temprana.

Los elementos anclados con prioridad NeverRemoveno se eliminan nunca. El código siguiente quita un elemento de caché y llama a Compact para quitar 25% de las entradas almacenadas en caché.

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

Para obtener más información, consulte la fuente Compact en GitHub.

Dependencias de caché

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

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

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

Notas adicionales

  • La caducidad no se produce en segundo plano. No hay ningún temporizador que examine activamente la memoria caché de los elementos expirados. Cualquier actividad de la memoria caché (Get, TryGetValue, Set, Remove) puede desencadenar un examen en segundo plano para los elementos expirados. Un temporizador en CancellationTokenSource (CancelAfter) también elimina la entrada y activa una búsqueda de elementos caducados. En el ejemplo siguiente se usa CancellationTokenSource(TimeSpan) para el token registrado. Cuando este token se activa, elimina la entrada inmediatamente y activa las devoluciones de llamada de desalojo:

    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);
    }
    
  • Cuando se usa un callback para reponer un elemento de caché:

    • Múltiples peticiones pueden encontrar el valor de la clave en caché vacío porque la llamada de retorno no se ha completado.
    • Esto puede dar lugar a que varios subprocesos repopulsen el elemento almacenado en caché.
  • Cuando una entrada de la caché se utiliza para crear otra, la entrada hija copia los tokens de caducidad y los ajustes de caducidad basados en el tiempo de la entrada primaria. El elemento secundario no caduca por la eliminación o actualización manual de la entrada primaria.

  • Utilice PostEvictionCallbacks para establecer las devoluciones de llamada que se activarán después de que la entrada de la caché sea desalojada de la caché.

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

Actualización de caché en segundo plano

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

Recursos adicionales

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 reduciendo el trabajo necesario para generar contenido. El almacenamiento en caché funciona mejor con los datos que cambian con poca frecuencia y es costoso 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 memoria caché más sencilla se basa en .IMemoryCache IMemoryCache representa una 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 persistentes al usar la 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é. En 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 caché en memoria puede almacenar cualquier objeto. La interfaz de caché distribuida está limitada a byte[]. El caché en memoria y el caché distribuido almacenan elementos como pares clave-valor.

System.Runtime.Caching/MemoryCache

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

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

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

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

Directrices de almacenamiento en 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 caché usa un recurso escaso, la memoria. Limitar el crecimiento de la memoria caché:
    • No use la entrada externa para las claves de caché.
    • Use expiraciones para limitar el crecimiento de la memoria caché.
    • Utilice SetSize, Size y SizeLimit para limitar el tamaño de la caché. El entorno de ejecución de ASP.NET Core no limita el tamaño de caché en función de la presión de memoria. El desarrollador tiene que limitar el tamaño de la memoria caché.

Uso de IMemoryCache

Advertencia

El uso de una caché de memoria compartida de la inserción de dependencias y realizar llamadas a SetSize, Size o SizeLimit para limitar el tamaño de la caché puede hacer que la aplicación falle. 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 caché compartida. Al usar SetSize, Sizeo SizeLimit para limitar la memoria 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 la caché. Una caché compartida es una compartida por 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 una hora no se almacena en caché, se crea una nueva entrada y se añade a la 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 Set extensión 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 en caché de DateTime permanece en la memoria caché mientras hay solicitudes dentro del período 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 Get a para capturar el tiempo almacenado 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 solo una expiración deslizante está en riesgo de que nunca expire. Si se accede repetidamente al elemento almacenado en caché dentro del intervalo de expiración deslizante, el elemento nunca expira. Combine una expiración deslizante con una expiración absoluta para garantizar que el elemento expira. La expiración absoluta establece un límite superior durante cuánto tiempo se puede almacenar en caché el elemento mientras se permite que el elemento expire antes si no se solicita dentro del intervalo de expiración deslizante. Si el intervalo de caducidad o el tiempo de caducidad absoluta pasan, el elemento se expulsa de la caché.

El código siguiente obtiene o crea un elemento almacenado en caché con 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, GetOrCreateAsyncy Get son métodos de extensión en la CacheExtensions clase . Estos métodos amplían la funcionalidad de IMemoryCache.

MemoryCacheEntryOptions

El ejemplo siguiente:

  • Establece una hora de expiración deslizante. Las solicitudes que acceden 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 que será llamado después de que la entrada sea desalojada de la caché. La devolución de llamada se ejecuta en un subproceso diferente del código que elimina el elemento de la 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 la caché

Opcionalmente, una MemoryCache 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 de ejecución de ASP.NET Core no limita el tamaño de la memoria en función de la presión de memoria. El desarrollador tiene que limitar el tamaño de la memoria caché. El tamaño especificado está en unidades que el desarrollador elige.

Por ejemplo:

  • Si la aplicación web estuviera almacenando en caché principalmente la cadena, cada entrada de caché podría tener como tamaño la longitud de 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 entorno de ejecución de ASP.NET Core 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 a Compact o Remove cuando la memoria disponible esté limitada:

El código siguiente crea un tamaño MemoryCache fijo sin unidad accesible 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 siguiente código registra MyMemoryCache con el contenedor de inyección de dependencia.

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

MyMemoryCache se crea como una caché de memoria independiente para los componentes que tienen en cuenta este tamaño limitado y saben cómo establecer el tamaño de entrada de caché de forma adecuada.

El código siguiente 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 métodos de SetSize 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 artículos caducados.
  • 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.
  • Objetos con la fecha de caducidad más temprana.

Los elementos anclados con prioridad NeverRemove nunca se eliminan. 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;

Para obtener más información, consulte la fuente Compact en GitHub.

Dependencias de caché

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

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 CancellationTokenSource permite expulsar varias entradas de caché como un grupo. Con el using patrón en el código anterior, las entradas de caché creadas dentro del using bloque heredarán los desencadenadores y la configuración de expiración.

Notas adicionales

  • La caducidad no se produce en segundo plano. No hay ningún temporizador que examine activamente la memoria caché de los elementos expirados. Cualquier actividad de la memoria caché (Get, Set, Remove) puede desencadenar un examen en segundo plano de los elementos expirados. Un temporizador en CancellationTokenSource (CancelAfter) también elimina la entrada y activa una búsqueda de elementos caducados. En el ejemplo siguiente se usa CancellationTokenSource(TimeSpan) para el token registrado. Cuando este token se activa, elimina la entrada inmediatamente y activa las devoluciones de llamada de desalojo:

    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 un callback para reponer un elemento de caché:

    • Múltiples peticiones pueden encontrar el valor de la clave en caché vacío porque la llamada de retorno no se ha completado.
    • Esto puede dar lugar a que varios subprocesos repopulsen el elemento almacenado en caché.
  • Cuando una entrada de la caché se utiliza para crear otra, la entrada hija copia los tokens de caducidad y los ajustes de caducidad basados en el tiempo de la entrada primaria. El elemento secundario no caduca por la eliminación o actualización manual de la entrada primaria.

  • Utilice PostEvictionCallbacks para establecer las devoluciones de llamada que se activarán después de que la entrada de la caché sea desalojada de la caché. En el código de ejemplo, se llama a CancellationTokenSource.Dispose() para liberar los recursos no administrados usados por CancellationTokenSource. Sin embargo, CancellationTokenSource no se elimina inmediatamente porque la entrada de caché todavía la usa. CancellationToken se pasa a MemoryCacheEntryOptions para crear una entrada de caché que expira después de un tiempo determinado. Por lo tanto, no se debe llamar a Dispose hasta que se quite o expire la entrada de caché. El código de ejemplo llama al método RegisterPostEvictionCallback para registrar una devolución de llamada que se invocará cuando se elimine la entrada de caché, y elimina el CancellationTokenSource dentro de esa devolución de llamada.

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

Actualización de caché en segundo plano

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

Recursos adicionales