Guía Paso a Paso: Caché Distribuida con Redis en ASP.NET y React
En el desarrollo de portales web modernos —especialmente aquellos con una alta carga visual e información estática como portales de turismo o catálogos— la velocidad de carga es crucial para la experiencia del usuario (UX) y el posicionamiento SEO. Consultar repetidamente una base de datos para obtener información que apenas varía ralentiza la aplicación y eleva innecesariamente los costes en la nube.
Para entenderlo de forma sencilla: imagina que trabajas en una biblioteca. Cada vez que un usuario te pide un mapa de Madrid, tienes que levantarte, ir al almacén del fondo, buscarlo y traérselo (esto sería una consulta tradicional a la base de datos). ¿No sería más eficiente dejar una copia del mapa en tu mostrador tras la primera búsqueda para entregarla instantáneamente a los siguientes usuarios? Eso es, exactamente, la memoria caché.
En este artículo técnico implementaremos paso a paso el patrón Cache-Aside utilizando una capa de Caché Distribuida con Redis, un backend en ASP.NET Core (Web API) y una interfaz moderna en React.
El Problema: Latencia en Consultas Repetitivas
Imagina un portal de turismo como Madrid Discovery. Cada vez que un usuario accede al sitio para ver los puntos de interés (el Museo del Prado, el Parque del Retiro, etc.), el frontend solicita los datos al backend.
En un entorno real con miles de usuarios concurrentes, esto provoca:
- Las llamadas continuas a la base de datos ralentizan el servidor.
- El tiempo de procesamiento (latencia) desmejora la experiencia del usuario.
- Se desperdician recursos de computación procesando la misma consulta una y otra vez.
Para simular este escenario del «mundo real» en nuestro entorno de desarrollo, hemos inducido un retardo artificial de 1.5 segundos en nuestro repositorio de datos.
La Solución: Caché Distribuida (Redis vs Memoria RAM)
Para solucionar esto de manera eficiente, aplicamos el patrón Cache-Aside:
- Cuando entra una petición, comprobamos si los datos ya están en nuestra memoria rápida (Redis).
- HIT (Acierto): Si están, los devolvemos instantáneamente.
- MISS (Fallo): Si no están, los consultamos a la base de datos lenta, los guardamos en Redis para el futuro y se los enviamos al usuario.
¿Por qué Redis y no la memoria local de .NET?
Si nuestro servidor backend necesita escalarse en la nube (creando múltiples instancias de la API), la caché local se quedaría aislada en cada servidor. Al usar Redis, todas las instancias comparten una única caché centralizada y externa.
Tutorial Paso a Paso
A continuación, detallamos la configuración completa del entorno y el backend.
Paso 1: Levantar Redis con Docker
Para no complicar la instalación en tu máquina local, utilizaremos un contenedor oficial de Redis. Asegúrate de tener Docker de escritorio activo y ejecuta el siguiente comando en tu terminal para levantar el servicio en el puerto estándar 6379:
docker run --name redis-madrid -p 6379:6379 -d redis
Paso 2: Instalar el paquete de NuGet en .NET
En tu proyecto Web API de ASP.NET Core, instala el cliente oficial de caché distribuida para Redis ejecutando:
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
(Este paquete nos permitirá comunicarnos de forma nativa con el contenedor que acabamos de levantar).
Paso 3: Configurar la Cadena de Conexión
Abre tu archivo de configuración appsettings.json y añade la dirección de tu Redis local:
{
"ConnectionStrings": {
"Redis": "localhost:6379"
}
}
Paso 4: Registrar el Servicio en Program.cs
Configuramos la inyección de dependencias en Program.cs. Añadiremos una lógica de fallback inteligente: si la cadena de conexión de Redis existe, la API se conectará al contenedor; si no, utilizará la memoria local de .NET de manera temporal para facilitar el desarrollo.
using MadridTurismo.API.Repositories;
var builder = WebApplication.CreateBuilder(args);
// Registrar controladores e inyectar el repositorio de datos de Madrid
builder.Services.AddControllers();
builder.Services.AddSingleton<IPlacesRepository, PlacesRepository>();
// Configurar Caché Distribuida con Fallback
string? redisConn = builder.Configuration.GetConnectionString("Redis");
if (!string.IsNullOrEmpty(redisConn))
{
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConn;
options.InstanceName = "MadridTurismo_";
});
Console.WriteLine($"Caché de Redis configurada en: {redisConn}");
}
else
{
builder.Services.AddDistributedMemoryCache(); // Fallback local si Docker está apagado
Console.WriteLine("Fallback activo: Usando caché en memoria de .NET");
}
// Configurar CORS para conectar de forma segura con React
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseCors("AllowFrontend");
app.UseAuthorization();
app.MapControllers();
app.Run();
Paso 5: Implementar la Lógica en el Controlador (.NET)
Inyectamos IDistributedCache en nuestro PlacesController. Como Redis trabaja únicamente con arreglos de bytes (byte[]), realizamos la serialización y deserialización en formato JSON (codificado en UTF-8) para guardar estructuras complejas.
Además, devolvemos propiedades de telemetría (ExecutionTimeMs y DataSource) para que el frontend pueda pintar en pantalla el origen exacto y la velocidad de los datos:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using MadridTurismo.API.Models;
using MadridTurismo.API.Repositories;
using System.Text.Json;
using System.Text;
namespace MadridTurismo.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class PlacesController : ControllerBase
{
private readonly IPlacesRepository _repository;
private readonly IDistributedCache _cache;
public PlacesController(IPlacesRepository repository, IDistributedCache cache)
{
_repository = repository;
_cache = cache;
}
[HttpGet]
public async Task<IActionResult> GetPlaces([FromQuery] string? category)
{
var stopwatch = System.Diagnostics.StartNew();
string cacheKey = string.IsNullOrEmpty(category)
? "places_all"
: $"places_cat_{category.ToLower()}";
// 1. Intentar recuperar desde caché de Redis
byte[]? cachedData = await _cache.GetAsync(cacheKey);
IEnumerable<TouristPlace>? places;
string dataSource;
if (cachedData != null)
{
// HIT de caché: Deserializamos los bytes a JSON
string json = Encoding.UTF8.GetString(cachedData);
places = JsonSerializer.Deserialize<IEnumerable<TouristPlace>>(json);
dataSource = "Redis Cache (RAM)";
}
else
{
// MISS de caché: Consultar Base de Datos
if (string.IsNullOrEmpty(category))
{
places = await _repository.GetPlacesAsync();
}
else
{
places = await _repository.GetPlacesByCategoryAsync(category);
}
// 2. Guardar en la caché por 2 minutos (Absolute Expiration)
string json = JsonSerializer.Serialize(places);
byte[] bytes = Encoding.UTF8.GetBytes(json);
var cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
};
await _cache.SetAsync(cacheKey, bytes, cacheOptions);
dataSource = "Base de Datos (Disco/Simulado)";
}
stopwatch.Stop();
return Ok(new
{
Data = places,
ExecutionTimeMs = stopwatch.ElapsedMilliseconds,
DataSource = dataSource,
Timestamp = DateTime.Now
});
}
[HttpPost("clear-cache")]
public async Task<IActionResult> ClearCache()
{
// Endpoint para la demo: permite limpiar la caché para forzar un "miss" manual
await _cache.RemoveAsync("places_all");
return Ok(new { Success = true, Message = "Caché de Redis invalidada." });
}
}
}
Paso 6: Consumir la API y Mostrar las Métricas en React
Para que nuestra interfaz muestre el Monitor de Rendimiento con los milisegundos reales, consumimos el endpoint de .NET guardando los metadatos del JSON en el estado de React.
Aquí tienes un ejemplo de cómo estructurar la petición en tu componente:
import { useState, useEffect } from 'react';
export function MadridDiscovery() {
const [places, setPlaces] = useState([]);
const [metrics, setMetrics] = useState({ time: 0, source: '' });
const fetchPlaces = async (category = '') => {
const url = category
? `http://localhost:5125/api/places?category=${category}`
: 'http://localhost:5125/api/places';
const response = await fetch(url);
const result = await response.json();
setPlaces(result.data);
setMetrics({
time: result.executionTimeMs,
source: result.dataSource
});
};
useEffect(() => { fetchPlaces(); }, []);
return (
<div>
{/* Monitor de Rendimiento */}
<div className="metrics-panel" style={{ padding: '15px', background: '#f0f4f8', borderRadius: '8px' }}>
<h3>📊 Monitor de Rendimiento</h3>
<p><strong>Origen:</strong> {metrics.source}</p>
<p><strong>Tiempo de Respuesta API:</strong> <span style={{ color: metrics.time > 1000 ? 'red' : 'green' }}>{metrics.time} ms</span></p>
<button onClick={() => fetchPlaces()}>Actualizar Petición</button>
</div>
{/* Galería de Tarjetas */}
<div className="grid">
{places.map(place => (
<div key={place.id} className="card">
<h4>{place.name}</h4>
<p>{place.description}</p>
</div>
))}
</div>
</div>
);
}
Resultados y Demostración en Vivo
Al interactuar con el frontend de React, el Monitor de Rendimiento refleja el cambio drástico de velocidad al instante:
- Carga en Frío (Fallo de Caché / MISS): La primera vez que entramos o tras reiniciar la caché, el servidor tarda 1508 ms debido a la simulación de lectura en disco de la Base de Datos.
- Carga en Caliente (Acierto de Caché / HIT): Al recargar o pulsar de nuevo el botón, los datos ya están en la RAM de Redis. El tiempo cae a 2 ms. Una gran reducción de velocidad del 99.8%
Prueba de Carga y Estrés con Autocannon
Para comprobar el comportamiento del servidor bajo condiciones extremas (como cientos de usuarios concurrentes ingresando a la web a la vez), realizamos un benchmark utilizando Autocannon (herramienta de benchmarking en Node.js de alto rendimiento).
Lanzamos la prueba simulando 50 usuarios concurrentes durante 5 segundos:
npx autocannon -c 50 -d 5 http://localhost:5125/api/places
Resultados de la Comparativa
| Métrica | Base de Datos (Caché Vacía / MISS) | Redis Cache (RAM / HIT) |
|---|---|---|
| Peticiones por Segundo (Avg) | 33 req/sec | 7,500+ req/sec |
| Latencia Media (Avg) | 1,510 ms | 6.2 ms |
| Transferencia de Datos | ~24 KB/sec | ~5.4 MB/sec |
- Sin Caché: El hilo del servidor se bloquea esperando la respuesta de la base de datos para cada usuario. La API colapsa procesando apenas 33 peticiones por segundo.
- Con Caché: Al servir directamente desde la memoria RAM de Redis, la API no sufre y procesa más de 7,000 peticiones por segundo sin despeinarse.
Obstáculos Superados y Recomendaciones
Serialización y tipos de datos complejos
IDistributedCache trabaja únicamente con arreglos de bytes (byte[]). Inicialmente, intentar guardar objetos complejos de forma directa produce errores de compilación. La solución óptima es transformar el objeto a string (JSON) con System.Text.Json y después convertirlo a binario usando Encoding.UTF8.GetBytes
Evitar Datos Obsoletos (Stale Data)
Si se añade un nuevo monumento en Madrid pero la caché sigue activa, los usuarios verán datos antiguos.
Recomendación: Utilizar tiempos de expiración absolutos razonables (de 2 a 5 minutos para contenido semiestático). Además, en los métodos de escritura (POST, PUT, DELETE) de tu API es obligatorio llamar explícitamente a _cache.RemoveAsync(cacheKey) para invalidar los datos antiguos en el mismo instante en que cambien.
Conclusiones e Interrogantes para Debate
La caché distribuida de Redis es una herramienta indispensable para garantizar la escalabilidad en sistemas Full Stack Cloud. Sin embargo, abre debates interesantes de diseño de arquitectura:
- ¿Compensa siempre asumir la latencia de red de un servidor externo como Redis frente a la velocidad de la memoria RAM local (DistributedMemoryCache), considerando proyectos pequeños?
- ¿Cómo diseñarías una estrategia de invalidación eficaz si cuentas con múltiples microservicios actualizando simultáneamente la misma base de datos distribuida?
Autor/a: Jorge Mori Felipe
Máster: Desarrollo Full Stack + Arquitecturas Cloud
Centro: Tajamar Tech
Año académico: 2025-2026
Código / recursos utilizados / Otros datos de interés: https://github.com/JorgeMori2/proyecto-tech-riders Almacenamiento en caché distribuido en ASP.NET Core | Microsoft Learn Distributed caching in ASP.NET Core | Microsoft Learn Docs