En numerosas ocasiones, se menciona la importancia de la caché en el desarrollo de aplicaciones. El almacenamiento en caché se destaca por su capacidad para mejorar el rendimiento y la escalabilidad de las aplicaciones, al reducir la carga de procesamiento y disminuir, en muchos casos, el tráfico de red. Aunque existen herramientas de caché robustas, como Redis, que contribuyen a lograr respuestas más rápidas, en esta publicación no exploraremos Redis. En cambio, nos enfocaremos en las opciones disponibles dentro del framework de .NET para obtener esta funcionalidad.
Caché en Memoria
En .NET, contamos con dos tipos de almacenamiento en caché disponibles para nuestras aplicaciones. En primer lugar, tenemos el caché de almacenamiento de salida, que nos permite almacenar dinámicamente controles y páginas en cualquier dispositivo compatible con HTTP 1.1. En subsiguientes solicitudes, no se ejecutará todo el código nuevamente; en su lugar, se retornará el resultado almacenado en el caché.
El segundo tipo disponible es el almacenamiento en caché de datos. Por ejemplo, al consultar una base de datos y obtener registros, los almacenamos en la memoria caché. En la siguiente consulta, verificamos si los datos están disponibles en caché; si es así, se devuelven; de lo contrario, se vuelven a consultar a la base de datos y se almacenan en caché para consultas futuras.
En la memoria caché, podemos almacenar cualquier tipo de objeto o colecciones de objetos, lo que contribuye a mejorar el rendimiento de nuestras aplicaciones. Sin embargo, es importante tener en cuenta que no es la opción ideal para aplicaciones distribuidas, ya que mantiene la caché en la instancia que se está invocando. Si contamos con múltiples servicios balanceados, la caché no estará presente simultáneamente en todos ellos.
Manos a la obra
Lo primero que haremos es crear un proyecto y agregar el desde el repositorio Nuget a la librería Microsoft.Extensions.Caching.Memory de la siguiente manera.
dotnet new webapp
dotnet add package Microsoft.Extensions.Caching.Memory
Una vez referido el paquete, para tener disponible el servicio de caché deberemos agregar el servicio por medio de inyección de dependencia. Para esto utilizaremos la interfaz IMemoryCache que será pasado por el constructor a los que deseen utilizarlos. Veamos se vería en una clase genérica:
using Microsoft.Extensions.Caching.Memory;
public class GenericClass
{
private readonly IMemoryCache _memoryCache;
public GenericClass(IMemoryCache memoryCache) =>
_memoryCache = memoryCache;
...
Para inyectar debemos ir a nuestro program.cs y agrega la línea:
builder.Services.AddMemoryCache();
Ahora veamos un código de caché implementado en alto nivel.
public async Task<List<Customer>> GetCustomersCacheAsync()
{
var output = _memoryCache.Get<List<Customer>>("customers");
if (output is not null) return output;
output = new()
{
new() { FirstName = "Homer", LastName = "Simpsons" },
new() { FirstName = "Moe", LastName = "Sislak" },
new() { FirstName = "Barny", LastName = "Gomez" }
};
_memoryCache.Set("customers", output, TimeSpan.FromMinutes(1));
return output;
Veamos paso a paso lo que hace nuestro método. La primera vez que se ejecuta intentara tomar del cache la lista de Customer (línea 3). Luego validará si no es null, como es la primera vez lo estará y seguirá para construir el objeto con los clientes, en nuestro caso el output. Aquí viene nuestra línea clave:
_memoryCache.Set("users", output, TimeSpan.FromMinutes(1));
En esta línea seteamos nuestro objeto en caché. Posee 3 partes, primero la key con la que reconoceremos a nuestro cache, luego lo que deseamos guardar, por último el tiempo que daemon guardarlo en memoria. Pasado este tiempo el caché se eliminará. Entonces, en una segunda invocación, al preguntar si es null, al no serlo, retorna directamente el output que almacenamos en la primera petición.
En un gráfico para ser más claros:
Existe un camino más eficiente para validar si un caché existe:
public async Task<List<Customer>> GetCustomersCacheAsync()
{
List<Customer> customers = null;
if (_memoryCache.TryGetValue("customers", out customers))
{
return customers;
}
output = new()
{
new() { FirstName = "Homer", LastName = "Simpsons" },
new() { FirstName = "Moe", LastName = "Sislak" },
new() { FirstName = "Barny", LastName = "Gomez" }
};
_memoryCache.Set("customers", output, TimeSpan.FromMinutes(1));
return output;
}
Hablemos un poco del tiempo de expiración: Sliding Expiration y Absolute Expiration. Sliding Expiration cadura el caché si no es accedido durante el tiempo establecido. Absolute Expiration el cache caducará en el tiempo establecido sin importar si se consume o no.
Para poder configurar alguno de estos 2 tipos usaremos el objeto MemoryCacheEntryOptions que nos permitirá configurar estas opciones veamos.
Sliding Expiration |
// Configurar cachevar cacheOptions = new MemoryCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromSeconds(30)); |
Absolute Expiration |
// Configurar cachevar cacheOptions = new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromSeconds(30)); |
por último:
// Setear el objeto
_memoryCache.Set("customers", output, cacheOptions);
¿Puedo guardar todo lo que quiera en la memoria caché? Si, pero tenemos que tener presente que la memoria de nuestros servidores son limitadas. Si llenamos la memoria podremos tener algunos inconvenientes en las respuestas de nuestra aplicación. Por esto es posible establecer un límite de uso de memoria para no crecer sin límite.
public class MyMemoryCache
{
public MemoryCache Cache { get; } = new MemoryCache(
new MemoryCacheOptions
{
SizeLimit = 1024
});
}
Conclusiones
La utilización de caché en memoria puede resultarnos sumamente beneficiosa al incrementar el rendimiento de nuestra aplicación y disminuir el tráfico de red dirigido hacia los repositorios de datos. Sin embargo, este recurso debe manejarse con precaución para evitar saturar toda la memoria disponible y también es crucial considerar el momento adecuado para la expiración del caché.
Aprovechar este mecanismo de almacenamiento en memoria puede optimizar la velocidad de acceso a datos previamente recuperados. No obstante, es fundamental establecer políticas de expiración adecuadas y gestionar la memoria de manera eficiente para garantizar un rendimiento óptimo.
Espero que hayas encontrado útil y esclarecedor el contenido de este post. Si tienes alguna pregunta o comentario adicional, no dudes en compartirlo. ¡Agradeceré tu feedback!