Oharra
Orrialde honetara sartzeak baimena behar du. Saioa hasteko edo direktorioak aldatzen saia zaitezke.
Orrialde honetara sartzeak baimena behar du. Direktorioak aldatzen saia zaitezke.
En este artículo, obtendrá información sobre varios mecanismos de almacenamiento en caché. El almacenamiento en caché es el acto de almacenar datos en una capa intermedia, lo que hace que las recuperaciones de datos posteriores sean más rápidas. Conceptualmente, el almacenamiento en caché es una estrategia de optimización del rendimiento y una consideración de diseño. El almacenamiento en caché puede mejorar significativamente el rendimiento de la aplicación al hacer que los datos que raramente cambian (o son difíciles de recuperar) estén más fácilmente disponibles. En este artículo se presentan tres enfoques de almacenamiento en caché y se proporciona código fuente de ejemplo para cada uno:
- Microsoft.Extensions.Caching.Memory: almacenamiento en caché en memoria para escenarios de servidor único
- Microsoft.Extensions.Caching.Hybrid: almacenamiento en caché híbrido que combina el almacenamiento en caché en memoria y distribuido con características adicionales.
- Microsoft.Extensions.Caching.Distributed: almacenamiento en caché distribuido para escenarios de varios servidores
Importante
Hay dos clases MemoryCache en .NET, una en el espacio de nombres System.Runtime.Caching, y otra en el espacio de nombres Microsoft.Extensions.Caching:
Aunque este artículo se centra en el almacenamiento en caché, no incluye el System.Runtime.Caching paquete NuGet. Todas las referencias a MemoryCache están dentro del Microsoft.Extensions.Caching espacio de nombres.
Todos los Microsoft.Extensions.* paquetes vienen listos para la inyección de dependencias (DI). Las IMemoryCacheinterfaces , HybridCachey IDistributedCache se pueden usar como servicios.
Almacenamiento en caché en memoria
En esta sección, obtendrá información sobre el paquete Microsoft.Extensions.Caching.Memory . La implementación actual de IMemoryCache es un contenedor alrededor de ConcurrentDictionary<TKey,TValue>, que expone una API rica en funciones. Las entradas de la memoria caché se representan mediante ICacheEntry y pueden ser cualquier object. La solución de caché en memoria es excelente para las aplicaciones que se ejecutan en un solo servidor, donde los datos almacenados en caché alquilan memoria en el proceso de la aplicación.
Sugerencia
Para escenarios de almacenamiento en caché de varios servidores, considere el enfoque de almacenamiento en caché distribuido como alternativa al almacenamiento en caché en memoria.
API de almacenamiento caché en memoria
El consumidor de la caché tiene control sobre las expiraciones deslizantes y absolutas:
- ICacheEntry.AbsoluteExpiration
- ICacheEntry.AbsoluteExpirationRelativeToNow
- ICacheEntry.SlidingExpiration
Si se establece una expiración, las entradas de la memoria caché se expulsarán si no se tiene acceso a ellas dentro de la asignación de tiempo de expiración. Los consumidores tienen opciones adicionales para controlar las entradas de caché, a través de MemoryCacheEntryOptions. Cada ICacheEntry se empareja con MemoryCacheEntryOptions, que expone la funcionalidad de expulsión de expiración con IChangeToken, la configuración de prioridad con CacheItemPriority, y el control de ICacheEntry.Size. Los métodos de extensión pertinentes son:
- MemoryCacheEntryExtensions.AddExpirationToken
- MemoryCacheEntryExtensions.RegisterPostEvictionCallback
- MemoryCacheEntryExtensions.SetSize
- MemoryCacheEntryExtensions.SetPriority
Ejemplo de caché en memoria
Para usar la implementación predeterminada IMemoryCache, llame al método de extensión AddMemoryCache para registrar todos los servicios necesarios con DI. En el ejemplo de código siguiente, se usa el host genérico para exponer la funcionalidad DI:
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();
Según la carga de trabajo de .NET, puede acceder a IMemoryCache de manera diferente, como con la inserción de constructores. En este ejemplo, utiliza la instancia IServiceProvider en el host y llama al método de extensión genérico GetRequiredService<T>(IServiceProvider).
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
Con los servicios de almacenamiento en caché en memoria registrados y resueltos a través de la inserción de dependencias, está listo para comenzar el almacenamiento en caché. Este ejemplo recorre en iteración las letras del alfabeto inglés "A" a "Z". El record AlphabetLetter tipo contiene la referencia a la letra y genera un mensaje.
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
Sugerencia
El modificador de acceso file se utiliza en el tipo AlphabetLetter, ya que está definido dentro del archivo Program.cs y solo se puede acceder a él desde allí. Para obtener más información, vea archivo (Referencia de C#). Para ver el código fuente completo, consulte la sección Program.cs .
El ejemplo incluye una función auxiliar que recorre en iteración las letras del alfabeto:
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
En el código de C# anterior:
- Se espera
Func<char, Task> asyncFuncen cada interacción y pasa el elementoletteractual. - Una vez procesadas todas las letras, se escribe una línea en blanco en la consola.
Para agregar elementos a la memoria caché, llame a una de las API: Create o Set.
var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
MemoryCacheEntryOptions options = new()
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};
_ = options.RegisterPostEvictionCallback(OnPostEviction);
AlphabetLetter alphabetLetter =
cache.Set(
letter, new AlphabetLetter(letter), options);
Console.WriteLine($"{alphabetLetter.Letter} was cached.");
return Task.Delay(
TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;
En el código de C# anterior:
- La variable
addLettersToCacheTaskdelega enIterateAlphabetAsyncy se espera. - Se agrega un argumento lambda a
Func<char, Task> asyncFunc. - Se crea una instancia de
MemoryCacheEntryOptionscon un vencimiento absoluto con respecto al ahora. - Se registra una devolución de llamada posterior a la expulsión.
- Se instancia un objeto
AlphabetLettery se pasa a Set junto conletteryoptions. - La letra se escribe en la consola como almacenada en caché.
- Por último, se devuelve Task.Delay.
Para cada letra del alfabeto, se escribe una entrada de caché con una expiración y una llamada de retorno posterior a la expulsión.
La callback posterior a la expulsión escribe los detalles del valor que ha sido expulsado en la consola.
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
Ahora que se rellena la memoria caché, se espera otra llamada a IterateAlphabetAsync , pero esta vez se llama a IMemoryCache.TryGetValue:
var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
if (cache.TryGetValue(letter, out object? value) &&
value is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
}
return Task.CompletedTask;
});
await readLettersFromCacheTask;
Si cache contiene la clave letter, y value es una instancia de AlphabetLetter, se escribe en la consola. Cuando la clave letter no está en la memoria caché, ha sido desalojada y se invocó su devolución de llamada después de la expulsión.
Métodos de extensión adicionales
IMemoryCache incluye muchos métodos de extensión orientados a la comodidad, incluido un método asincrónico GetOrCreateAsync:
- CacheExtensions.Get
- CacheExtensions.GetOrCreate
- CacheExtensions.GetOrCreateAsync
- CacheExtensions.Set
- CacheExtensions.TryGetValue
Ponlo todo junto
Todo el código fuente de la aplicación de ejemplo es un programa de nivel superior y requiere dos paquetes NuGet:
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
const int MillisecondsDelayAfterAdd = 50;
const int MillisecondsAbsoluteExpiration = 750;
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
MemoryCacheEntryOptions options = new()
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};
_ = options.RegisterPostEvictionCallback(OnPostEviction);
AlphabetLetter alphabetLetter =
cache.Set(
letter, new AlphabetLetter(letter), options);
Console.WriteLine($"{alphabetLetter.Letter} was cached.");
return Task.Delay(
TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;
var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
if (cache.TryGetValue(letter, out object? value) &&
value is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
}
return Task.CompletedTask;
});
await readLettersFromCacheTask;
await host.RunAsync();
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
Puede ajustar los valores MillisecondsDelayAfterAdd y MillisecondsAbsoluteExpiration para observar los cambios en el comportamiento de la expiración y expulsión de entradas almacenadas en caché. A continuación se muestra la salida de ejemplo de la ejecución de este código. (Debido a la naturaleza no determinista de los eventos de .NET, la salida podría ser diferente).
A was cached.
B was cached.
C was cached.
D was cached.
E was cached.
F was cached.
G was cached.
H was cached.
I was cached.
J was cached.
K was cached.
L was cached.
M was cached.
N was cached.
O was cached.
P was cached.
Q was cached.
R was cached.
S was cached.
T was cached.
U was cached.
V was cached.
W was cached.
X was cached.
Y was cached.
Z was cached.
A was evicted for Expired.
C was evicted for Expired.
B was evicted for Expired.
E was evicted for Expired.
D was evicted for Expired.
F was evicted for Expired.
H was evicted for Expired.
K was evicted for Expired.
L was evicted for Expired.
J was evicted for Expired.
G was evicted for Expired.
M was evicted for Expired.
N was evicted for Expired.
I was evicted for Expired.
P was evicted for Expired.
R was evicted for Expired.
O was evicted for Expired.
Q was evicted for Expired.
S is still in cache. The 'S' character is the 19 letter in the English alphabet.
T is still in cache. The 'T' character is the 20 letter in the English alphabet.
U is still in cache. The 'U' character is the 21 letter in the English alphabet.
V is still in cache. The 'V' character is the 22 letter in the English alphabet.
W is still in cache. The 'W' character is the 23 letter in the English alphabet.
X is still in cache. The 'X' character is the 24 letter in the English alphabet.
Y is still in cache. The 'Y' character is the 25 letter in the English alphabet.
Z is still in cache. The 'Z' character is the 26 letter in the English alphabet.
Dado que se establece la expiración absoluta (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow), todos los elementos almacenados en caché finalmente se expulsarán.
Almacenamiento en caché del servicio de trabajo
Una estrategia común para almacenar datos en caché es actualizar la memoria caché independientemente de los servicios de datos que consumen. La plantilla Servicio de Trabajador es un excelente ejemplo, ya que BackgroundService funciona independientemente (o en segundo plano) del código de la otra aplicación. Cuando una aplicación comienza a ejecutarse y hospeda una implementación de IHostedService, la implementación correspondiente (en este caso, el BackgroundService o "trabajador") comienza a ejecutarse en el mismo proceso. Estos servicios hospedados se registran en la inserción de dependencias como singletons, mediante el método de extensión AddHostedService<THostedService>(IServiceCollection). Otros servicios se pueden registrar en DI con cualquier tipo de duración del servicio.
Importante
Las duraciones del servicio son importantes para comprender. Al llamar a AddMemoryCache para registrar todos los servicios de almacenamiento en caché en memoria, los servicios se registran como singletons.
Escenario de servicio de fotos
Imagine que está desarrollando un servicio de fotos que se basa en la API de terceros accesible a través de HTTP. Estos datos fotográficos no cambian a menudo, pero hay una gran cantidad de ellos. Cada foto se representa mediante un sencillo record:
namespace CachingExamples.Memory;
public readonly record struct Photo(
int AlbumId,
int Id,
string Title,
string Url,
string ThumbnailUrl);
En el ejemplo siguiente, ves varios servicios registrados con DI. Cada servicio tiene una sola responsabilidad.
using CachingExamples.Memory;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<CacheWorker>();
builder.Services.AddHostedService<CacheWorker>();
builder.Services.AddScoped<PhotoService>();
builder.Services.AddSingleton(typeof(CacheSignal<>));
using IHost host = builder.Build();
await host.StartAsync();
En el código de C# anterior:
- El host genérico se crea con los valores predeterminados.
- Los servicios de almacenamiento en caché en memoria se registran con AddMemoryCache.
- Se registra una instancia de
HttpClientpara la claseCacheWorkercon AddHttpClient<TClient>(IServiceCollection). - La
CacheWorkerclase se registra con AddHostedService<THostedService>(IServiceCollection). - La
PhotoServiceclase se registra con AddScoped<TService>(IServiceCollection). - La
CacheSignal<T>clase se registra con AddSingleton. -
hostse instancia a partir del constructor y se inicia de forma asincrónica.
El PhotoService es responsable de obtener fotos que sean similares a criterios dados (o filter):
using Microsoft.Extensions.Caching.Memory;
namespace CachingExamples.Memory;
public sealed class PhotoService(
IMemoryCache cache,
CacheSignal<Photo> cacheSignal,
ILogger<PhotoService> logger)
{
public async IAsyncEnumerable<Photo> GetPhotosAsync(Func<Photo, bool>? filter = default)
{
try
{
await cacheSignal.WaitAsync();
Photo[] photos =
(await cache.GetOrCreateAsync(
"Photos", _ =>
{
logger.LogWarning("This should never happen!");
return Task.FromResult(Array.Empty<Photo>());
}))!;
// If no filter is provided, use a pass-thru.
filter ??= _ => true;
foreach (Photo photo in photos)
{
if (!default(Photo).Equals(photo) && filter(photo))
{
yield return photo;
}
}
}
finally
{
cacheSignal.Release();
}
}
}
En el código de C# anterior:
- El constructor requiere un
IMemoryCache,CacheSignal<Photo>yILogger. - El método
GetPhotosAsyncrealiza las acciones siguientes:- Define un
Func<Photo, bool> filterparámetro y devuelve unIAsyncEnumerable<Photo>. - Llama a y espera a que
_cacheSignal.WaitAsync()se libere; esto garantiza que la memoria caché se rellene antes de acceder a la memoria caché. - Llama a
_cache.GetOrCreateAsync(), obteniendo de forma asincrónica todas las fotos de la memoria caché. - El
factoryargumento registra una advertencia y devuelve una matriz de fotos vacía; esto nunca debería ocurrir. - Cada foto de la memoria caché se itera, filtra y materializa con
yield return. - Por último, se restablece la señal de caché.
- Define un
Los consumidores de este servicio pueden llamar al método GetPhotosAsync y controlar las fotos en consecuencia. No HttpClient es necesario, ya que la memoria caché contiene las fotos.
La señal asincrónica se basa en una instancia encapsulada SemaphoreSlim , dentro de un singleton restringido de tipo genérico. La CacheSignal<T> se basa en una instancia de SemaphoreSlim:
namespace CachingExamples.Memory;
public sealed class CacheSignal<T>
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
/// <summary>
/// Exposes a <see cref="Task"/> that represents the asynchronous wait operation.
/// When signaled (consumer calls <see cref="Release"/>), the
/// <see cref="Task.Status"/> is set as <see cref="TaskStatus.RanToCompletion"/>.
/// </summary>
public Task WaitAsync() => _semaphore.WaitAsync();
/// <summary>
/// Exposes the ability to signal the release of the <see cref="WaitAsync"/>'s operation.
/// Callers who were waiting, will be able to continue.
/// </summary>
public void Release() => _semaphore.Release();
}
En el código de C# anterior, el patrón de decorador se usa para encapsular una instancia de .SemaphoreSlim Puesto que CacheSignal<T> se registra como singleton, se puede usar en todas las duraciones del servicio con cualquier tipo genérico, en este caso, Photo. Es responsable de indicar la inicialización de la memoria caché.
CacheWorker es una subclase de BackgroundService:
using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;
namespace CachingExamples.Memory;
public sealed class CacheWorker(
ILogger<CacheWorker> logger,
HttpClient httpClient,
CacheSignal<Photo> cacheSignal,
IMemoryCache cache) : BackgroundService
{
private readonly TimeSpan _updateInterval = TimeSpan.FromHours(3);
private bool _isCacheInitialized = false;
private const string Url = "https://jsonplaceholder.typicode.com/photos";
public override async Task StartAsync(CancellationToken cancellationToken)
{
await cacheSignal.WaitAsync();
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
logger.LogInformation("Updating cache.");
try
{
Photo[]? photos =
await httpClient.GetFromJsonAsync<Photo[]>(
Url, stoppingToken);
if (photos is { Length: > 0 })
{
cache.Set("Photos", photos);
logger.LogInformation(
"Cache updated with {Count:#,#} photos.", photos.Length);
}
else
{
logger.LogWarning(
"Unable to fetch photos to update cache.");
}
}
finally
{
if (!_isCacheInitialized)
{
cacheSignal.Release();
_isCacheInitialized = true;
}
}
try
{
logger.LogInformation(
"Will attempt to update the cache in {Hours} hours from now.",
_updateInterval.Hours);
await Task.Delay(_updateInterval, stoppingToken);
}
catch (OperationCanceledException)
{
logger.LogWarning("Cancellation acknowledged: shutting down.");
break;
}
}
}
}
En el código de C# anterior:
- El constructor requiere un
ILogger,HttpClientyIMemoryCache. - El
_updateIntervalestá definido por tres horas. - El método
ExecuteAsyncrealiza las acciones siguientes:- Se recorre en bucle mientras la aplicación está en ejecución.
- Realiza una solicitud HTTP a
"https://jsonplaceholder.typicode.com/photos"y asigna la respuesta como una matriz dePhotoobjetos. - La matriz de fotos se coloca en el
IMemoryCachebajo la clave"Photos". - Se llama a
_cacheSignal.Release(), lo que libera a todos los consumidores que estaban esperando la señal. - Se espera la llamada a Task.Delay, dado el intervalo de actualización.
- Después de retrasar durante tres horas, la memoria caché se actualiza de nuevo.
Los consumidores del mismo proceso podrían solicitar a IMemoryCache las fotos, pero CacheWorker es responsable de actualizar la memoria caché.
Almacenamiento en caché híbrido
La HybridCache biblioteca combina las ventajas del almacenamiento en caché distribuido y en memoria, a la vez que aborda los desafíos comunes con las API de almacenamiento en caché existentes. Introducido en .NET 9, HybridCache proporciona una API unificada que simplifica la implementación del almacenamiento en caché e incluye características integradas, como la protección de stampede y la serialización configurable.
Características clave
HybridCache ofrece varias ventajas sobre el uso IMemoryCache y IDistributedCache por separado:
- Almacenamiento en caché de dos niveles: administra automáticamente las capas de caché en memoria (L1) y distribuidas (L2). Los datos se recuperan primero de la memoria caché en memoria para mayor rapidez, luego de la caché distribuida si es necesario, y finalmente del origen.
- Protección de Stampede: impide que varias solicitudes simultáneas ejecuten la misma operación costosa. Solo una solicitud captura los datos mientras otros esperan el resultado.
- Serialización configurable: admite varios formatos de serialización, incluidos JSON (valor predeterminado), protobuf y XML.
- Invalidación basada en etiquetas: agrupa entradas de caché relacionadas con etiquetas para una invalidación por lotes eficaz.
-
API simplificada: el
GetOrCreateAsyncmétodo controla automáticamente errores de caché, serialización y almacenamiento.
Cuándo usar HybridCache
Considere la posibilidad de usar HybridCache cuando:
- Necesita tanto el almacenamiento en caché local (en memoria) como el almacenamiento en caché distribuido en un entorno de varios servidores.
- Quiere protección frente a situaciones de avalancha de caché.
- Prefiere una API simplificada sobre la coordinación manual de
IMemoryCacheyIDistributedCache. - Necesitas invalidación de caché con etiquetas para entradas relacionadas.
Sugerencia
En el caso de las aplicaciones de servidor único con necesidades sencillas de almacenamiento en caché, el almacenamiento en caché en memoria podría ser suficiente. En el caso de las aplicaciones multiservidor sin necesidad de protección contra stampede o invalidación basada en etiquetas, considere la posibilidad de almacenar en caché distribuida.
Configuración de HybridCache
Para usar HybridCache, instale el Microsoft.Extensions.Caching.Hybrid paquete NuGet:
dotnet add package Microsoft.Extensions.Caching.Hybrid
Registre el servicio HybridCache con DI mediante una llamada a AddHybridCache:
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHybridCache();
El código anterior registra HybridCache con las opciones predeterminadas. También puede configurar opciones globales:
var builderWithOptions = Host.CreateApplicationBuilder(args);
builderWithOptions.Services.AddHybridCache(options =>
{
options.MaximumPayloadBytes = 1024 * 1024; // 1 MB
options.MaximumKeyLength = 1024;
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
});
Uso básico
El método principal para interactuar con HybridCache es GetOrCreateAsync. Este método comprueba la memoria caché de una entrada con la clave especificada y, si no se encuentra, llama al método factory para recuperar los datos:
async Task<WeatherData> GetWeatherDataAsync(HybridCache cache, string city)
{
return await cache.GetOrCreateAsync(
$"weather:{city}",
async cancellationToken =>
{
// Simulate fetching from an external API
await Task.Delay(100, cancellationToken);
return new WeatherData(city, 72, "Sunny");
}
);
}
En el código de C# anterior:
- El
GetOrCreateAsyncmétodo toma una clave única y un método de fábrica. - Si los datos no están en la memoria caché, se llama al método factory para recuperarlos.
- Los datos se almacenan automáticamente en cachés en memoria y distribuidas.
- Solo una solicitud simultánea ejecuta el método factory; otros esperan el resultado.
Opciones de entrada
Puede invalidar los valores predeterminados globales para entradas de caché específicas mediante HybridCacheEntryOptions:
async Task<WeatherData> GetWeatherWithOptionsAsync(HybridCache cache, string city)
{
var entryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
return await cache.GetOrCreateAsync(
$"weather:{city}",
async cancellationToken => new WeatherData(city, 72, "Sunny"),
entryOptions
);
}
Las opciones de entrada permiten configurar:
- HybridCacheEntryOptions.Expiration: cuánto tiempo se debe almacenar en caché la entrada en la caché distribuida.
- HybridCacheEntryOptions.LocalCacheExpiration: cuánto tiempo se debe almacenar en caché la entrada en la memoria local.
- HybridCacheEntryOptions.Flags: marcas adicionales para controlar el comportamiento de la caché.
Invalidación basada en etiquetas
Las etiquetas permiten agrupar las entradas de caché relacionadas y invalidarlas juntas. Esto es útil para escenarios en los que los datos relacionados deben actualizarse como una unidad:
async Task<CustomerData> GetCustomerAsync(HybridCache cache, int customerId)
{
var tags = new[] { "customer", $"customer:{customerId}" };
return await cache.GetOrCreateAsync(
$"customer:{customerId}",
async cancellationToken => new CustomerData(customerId, "John Doe", "john@example.com"),
new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(30) },
tags
);
}
Para invalidar todas las entradas con una etiqueta específica:
async Task InvalidateCustomerCacheAsync(HybridCache cache, int customerId)
{
await cache.RemoveByTagAsync($"customer:{customerId}");
}
También puede invalidar varias etiquetas a la vez:
async Task InvalidateAllCustomersAsync(HybridCache cache)
{
await cache.RemoveByTagAsync(new[] { "customer", "orders" });
}
Nota:
La invalidación basada en etiquetas es una operación lógica. No quita activamente los valores de la memoria caché, pero garantiza que las entradas etiquetadas se tratan como errores de caché. Las entradas expiran finalmente en función de su duración configurada.
Quitar entradas de caché
Para quitar una entrada de caché específica por clave, use el RemoveAsync método :
async Task RemoveWeatherDataAsync(HybridCache cache, string city)
{
await cache.RemoveAsync($"weather:{city}");
}
Para invalidar todas las entradas almacenadas en caché, use la etiqueta comodín reservada "*":
async Task InvalidateAllCacheAsync(HybridCache cache)
{
await cache.RemoveByTagAsync("*");
}
Serialización
Para escenarios de almacenamiento en caché distribuido, HybridCache requiere serialización. De forma predeterminada, controla string e byte[] internamente y usa System.Text.Json para otros tipos. Puede configurar serializadores personalizados para tipos específicos o usar un serializador de uso general:
// Custom serialization example
// Note: This requires implementing a custom IHybridCacheSerializer<T>
var builderWithSerializer = Host.CreateApplicationBuilder(args);
builderWithSerializer.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
// To add a custom serializer, uncomment and provide your implementation:
// .AddSerializer<WeatherData, CustomWeatherDataSerializer>();
Configuración de la caché distribuida
HybridCache usa la implementación configurada IDistributedCache para su caché distribuida (L2). Incluso sin un IDistributedCache configurado, HybridCache todavía proporciona almacenamiento en caché en memoria y protección contra avalanchas. Para agregar Redis como caché distribuida:
// Distributed cache with Redis
var builderWithRedis = Host.CreateApplicationBuilder(args);
builderWithRedis.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
builderWithRedis.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
Para obtener más información sobre las implementaciones de caché distribuida, consulte Almacenamiento en caché distribuido.
Almacenamiento en caché distribuido
En algunos escenarios, se requiere una caché distribuida, como sucede con varios servidores de aplicaciones. Una caché distribuida admite un escalado horizontal mayor que el enfoque de almacenamiento en caché en memoria. El uso de una caché distribuida descarga la memoria caché en un proceso externo, pero requiere E/S de red adicional e introduce un poco más de latencia (incluso si es nominal).
Las abstracciones de almacenamiento en caché distribuidas forman parte del Microsoft.Extensions.Caching.Memory paquete NuGet y incluso hay un AddDistributedMemoryCache método de extensión.
Precaución
AddDistributedMemoryCache solo debe usarse en escenarios de desarrollo o pruebas y no es una implementación de producción viable.
Tenga en cuenta cualquiera de las implementaciones disponibles de IDistributedCache de los siguientes paquetes.
Microsoft.Extensions.Caching.SqlServerMicrosoft.Extensions.Caching.StackExchangeRedisNCache.Microsoft.Extensions.Caching.OpenSource
API de almacenamiento en caché distribuida
Las API de almacenamiento en caché distribuidas son un poco más primitivas que sus homólogos de API de almacenamiento en caché en memoria. Los pares clave-valor son un poco más básicos. Las claves de almacenamiento en caché en memoria se basan en un object, mientras que las claves distribuidas son una string. Con el almacenamiento en caché en memoria, el valor puede ser cualquier genérico fuertemente tipado, mientras que los valores del almacenamiento en caché distribuido se conservan como byte[]. Esto no significa que varias implementaciones no expongan valores genéricos fuertemente tipados, pero es un detalle de implementación.
Creación de valores
Para crear valores en la caché distribuida, llame a una de las API establecidas:
Con el AlphabetLetter registro del ejemplo de caché en memoria, puede serializar el objeto en JSON y, a continuación, codificar string como :byte[]
DistributedCacheEntryOptions options = new()
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};
AlphabetLetter alphabetLetter = new(letter);
string json = JsonSerializer.Serialize(alphabetLetter);
byte[] bytes = Encoding.UTF8.GetBytes(json);
await cache.SetAsync(letter.ToString(), bytes, options);
De manera muy similar al almacenamiento en caché en memoria, las entradas de caché pueden tener opciones para ayudar a ajustar su existencia en la memoria caché, en este caso, DistributedCacheEntryOptions.
Creación de métodos de extensión
Hay varios métodos de extensión orientados a la conveniencia para crear valores. Estos métodos ayudan a evitar la codificación string de representaciones de objetos en :byte[]
Leer valores
Para leer valores de la caché distribuida, llame a una de las Get API:
AlphabetLetter? alphabetLetter = null;
byte[]? bytes = await cache.GetAsync(letter.ToString());
if (bytes is { Length: > 0 })
{
string json = Encoding.UTF8.GetString(bytes);
alphabetLetter = JsonSerializer.Deserialize<AlphabetLetter>(json);
}
Una vez leída una entrada de la caché, puede obtener la representación codificada en UTF8 desde el byte[].
Lectura de métodos de extensión
Hay varios métodos de extensión basados en conveniencia para leer valores. Estos métodos ayudan a evitar la descodificación byte[] en las string representaciones de objetos:
Actualizar valores
No hay ninguna manera de actualizar los valores de la caché distribuida con una sola llamada API. En su lugar, los valores pueden restablecer sus expiraciones deslizantes con una de las API de actualización:
Si es necesario actualizar el valor real, debe eliminar el valor y volver a agregarlo.
Eliminar valores
Para eliminar valores en la caché distribuida, llame a una de las Remove API:
Sugerencia
Aunque hay versiones sincrónicas de estas API, tenga en cuenta el hecho de que las implementaciones de caché distribuidas dependen de la E/S de red. Por este motivo, normalmente es preferible usar las API asincrónicas.