Hay varios factores que afectan al rendimiento de la aplicación: llamadas a la base de datos, latencia de la red o servicios de terceros. Cuando nuestras aplicaciones crecen lo suficiente en cantidad de usuarios concurrentes vuelven a tenderse lentas. Para aumentar el rendimiento podemos usar Memoria Caché. Veamos que es cache, porque usarlo y cómo implementarlo en memoria de la Mano de .Net y Minimal API
¿Qué es Caché?
El almacenamiento en caché es la técnica de almacenamiento de los datos que se utilizan con mayor frecuencia. No se guardan todos los datos, si no los que realmente se consumen con concurrencia. Estos datos son guardados en memoria, lo cual, hace que sea más rápido debido a que la mayoría tiene un menor tiempo de acceso que un disco duro, por ejemplo.
¿Por qué usarlo?
La idea principal del uso es reducir la cantidad de salidas desde nuestros servicios hacia bases de datos o servicios de terceros buscando evitar la red o los tiempos de los orígenes de datos que vamos a consumir.
Supongamos que tenemos datos que no cambian en ningún momento. Por ejemplo, las provincias o estados de un país. No es necesario ir a buscarlos a la base de datos todo el tiempo, si no cambian, podemos almacenarlos en memoria caché para tener un mayor acceso y reducir el consumo de la base de datos o servicio.
El otro escenario es que los datos cambian una cierta cantidad de tiempo. Por ejemplo, si los productos de una compañía cambian cada 1 hora, podemos decirle al caché que en una hora sea eliminado obligándolo a ir a la base de datos para volver a traer los datos.
Veamos el diagrama de cómo funciona básicamente:
En el diagrama podemos ver cómo funciona, un usuario pide algún dato a la WebApi. Esta se fija si tiene lo solicitado en la memoria caché, si lo tiene, lo recupera y lo devuelve. En caso de no tener los datos en caché, los irá a buscar a la base de datos, los almacena en el caché para próximas consultas y le retornará la info al usuario.
Existen básicamente 3 tipos de caché:
- En memoria, usa la misma memoria del servidor donde se encuentran alojados nuestros servicios.
- Caché persistente, en alguna base de datos o archivos (menos utilizado)
- Cache distribuido, se guarda en varios servidores. El más usado es Redis.
Nosotros veremos caché en memoria. Pero no todo lo que brilla es oro, tenemos que tener presente que hay pros y contras:
Pros | Contras |
Es más fácil y rápido que otros mecanismos de almacenamiento en caché. | Si el caché no está configurado correctamente, puede consumir los recursos del servidor. |
Reduce la carga en servicios web/base de datos | Mayor Mantenimiento. |
Aumentar el rendimiento | Problemas de escalabilidad. Es adecuado para un solo servidor. Si tenemos muchos servidores, no podemos compartir el caché con todos los servidores. |
Altamente fiable | |
Es adecuado para aplicaciones pequeñas y medianas |
¿Cómo lo implementamos?
Vamos a lo más divertido, el código. Utilizaremos Visual Studio 2022 y .Net 6. Lo primero que haremos es crear nuestro proyecto para eso, seleccionamos Asp.Net Core Web API. En la tercera pantalla daremos nombre a nuestro proyecto, y en la cuarta no debemos olvidar destildar la opción “usar controladores…” para que el proyecto sea Minimal API.
Lo siguiente será agregar la extensión Microsoft.Extensions.Caching.Memory desde el repositorio de paquetes NuGet. Vamos a nuestro proyecto, botón derecho y selecciona administrar paquetes NuGet.
En examinar buscamos Microsoft.Extensions.Caching.Memory y lo instalamos.
Vamos a crear un contexto de base de datos y unos modelos. Abrimos nuestro archivo program.cs y luego de app.Run() agregaremos el siguiente código.
class Product
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsActive{ get; set; }
}
class ProductDb : DbContext
{
public ProductDb (DbContextOptions<ProductDb > options)
: base(options) { }
public DbSet<Product> Products=> Set<Product>();
}
También agregaremos Microsoft.EntityFrameworkCore.InMemory desde los paquetes NuGet. Con este componeten podremos usar en memoria nuestra base de datos.
Luego la inyectamos como servicio.
builder.Services.AddDbContext<ProductDb>(opt => opt.UseInMemoryDatabase("ProductDb"));
Ahora, antes del App.Run, mapeamos la solicitud del usuario para darle respuesta. Completamos con el siguiente código.
app.MapGet("/products", async (ProductDb db) =>
await db.Products.ToListAsync());
Hasta el momento, no hemos implementado cache, solamente hicimos referencia a la extensión. Debemos agregar el servicio de caché para eso lo haremos antes de la línea builder.Build(); la siguiente línea:
// Add the memory cache services.
builder.Services.AddMemoryCache();
Lo siguiente es modificar nuestro código de la API:
app.MapGet("/products", async (ProductDb db, IMemoryCache memoryCache) =>
{
IList<Product> _products;
if (!memoryCache.TryGetValue("ProductList", out _products))
{
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(3));
_products = db.Products.ToList();
memoryCache.Set("ProductList", _products, cacheEntryOptions);
}
return _products;
});
Vemos que nuestro api inyecta la base de datos y también el caché en memoria. Básicamente lo que hace es validar si está en caché por medio del método TryGetValue. Cada objeto que guardamos en el cache tiene un Jey, en nuestro caso ProductList. El siguiente parámetro es salida, prestemos atención que está declarado como out. Esto quiere decir que sí está el resultado lo guarda en la variable que declaramos, en caso de que no esté procederá a ejecutar el código, configurando 3 segundos de expiración, buscará en la base de datos, guarda en el cache y retorna el resultado.
Les dejo el código completo.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddMemoryCache();
builder.Services.AddDbContext<ProductDb>(opt => opt.UseInMemoryDatabase("ProductDb"));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/products", async (ProductDb db, IMemoryCache memoryCache) =>
{
IList<Product> _products;
if (!memoryCache.TryGetValue("ProductList", out _products))
{
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(3));
_products = db.Products.ToList();
memoryCache.Set("ProductList", _products, cacheEntryOptions);
}
return _products;
});
app.Run();
class Product
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsActive { get; set; }
}
class ProductDb : DbContext
{
public ProductDb(DbContextOptions<ProductDb> options)
: base(options) { }
public DbSet<Product> Products => Set<Product>();
}
Conclusiones
Estas es una buena opción cuando tenemos mucha concurrencia o cuando hay datos que no cambian nunca logrando evitar llamadas innecesarias a base de datos o servicios. En futuros post veremos otras formas de manejar caché y los tipos de caché que existen.