Implementar Cache en una API REST con Redis

Lectura
25 min~6 min lectura

Concepto clave

Implementar cache con Redis en una API REST es una estrategia fundamental para mejorar el rendimiento de aplicaciones backend. En esencia, el cache actúa como una capa de almacenamiento temporal de alta velocidad que guarda respuestas frecuentemente solicitadas, reduciendo la carga en bases de datos primarias y acelerando los tiempos de respuesta.

Imagina un restaurante muy concurrido donde los platos más populares se preparan en pequeñas cantidades y se mantienen listos para servir, en lugar de cocinarlos desde cero para cada pedido. Redis funciona de manera similar: almacena datos como respuestas JSON de endpoints API, evitando costosas consultas a bases de datos para solicitudes idénticas. Esto es especialmente crítico en escenarios de alta concurrencia, donde cada milisegundo cuenta.

El ciclo básico de cache implica: 1) Verificar si la respuesta existe en Redis, 2) Si existe, devolverla inmediatamente, 3) Si no existe, obtener los datos de la fuente original (ej., base de datos), 4) Almacenar la respuesta en Redis con un tiempo de expiración (TTL), y 5) Devolver la respuesta al cliente. Este patrón, conocido como cache-aside o lazy loading, es el más común en APIs REST.

Cómo funciona en la práctica

Veamos un ejemplo paso a paso para implementar cache en un endpoint de API que obtiene detalles de usuario. Supongamos una API REST construida con Node.js y Express, usando Redis como servidor de cache.

  1. Configuración inicial: Conecta tu aplicación a Redis usando una biblioteca como ioredis o redis. Establece un cliente Redis que maneje conexiones y reconexiones automáticas.
  2. Diseño de clave: Define una clave única para cada recurso cacheado. Por ejemplo, para un usuario con ID 123, usa user:123. Esto asegura que no haya colisiones y facilita la invalidación.
  3. Implementación del middleware o lógica de cache: En el controlador del endpoint GET /users/:id, añade la lógica para verificar primero en Redis.
  4. Manejo de expiración: Configura un TTL (Time To Live) adecuado, como 300 segundos, para que los datos no se vuelvan obsoletos indefinidamente.
  5. Invalidación: Actualiza o elimina la clave en Redis cuando los datos del usuario cambian (ej., en endpoints PUT /users/:id), para mantener la consistencia.

Aquí un fragmento de código ilustrativo:

async function getUser(req, res) {
  const userId = req.params.id;
  const cacheKey = `user:${userId}`;
  
  // Paso 1: Intentar obtener del cache
  const cachedUser = await redisClient.get(cacheKey);
  if (cachedUser) {
    return res.json(JSON.parse(cachedUser));
  }
  
  // Paso 2: Si no está en cache, consultar base de datos
  const user = await db.users.findById(userId);
  if (!user) {
    return res.status(404).json({ error: 'Usuario no encontrado' });
  }
  
  // Paso 3: Almacenar en cache con TTL
  await redisClient.setex(cacheKey, 300, JSON.stringify(user));
  
  // Paso 4: Devolver respuesta
  res.json(user);
}

Caso de estudio

Considera una plataforma de e-commerce con un endpoint GET /products/:id que muestra detalles de productos. Sin cache, cada solicitud genera una consulta SQL a una base de datos PostgreSQL, lo que bajo carga de 1000 solicitudes por segundo causa latencias de 200ms y alta carga en la BD.

Al implementar cache con Redis:

  • Se define una clave como product:${id} para cada producto.
  • Se configura un TTL de 600 segundos, ya que los productos cambian ocasionalmente.
  • Se añade lógica para invalidar el cache cuando un producto se actualiza (ej., en PUT /products/:id).

Resultados medidos en producción:

MétricaSin CacheCon Cache
Tiempo de respuesta promedio200ms5ms
Carga en base de datos1000 consultas/seg50 consultas/seg
Disponibilidad API95%99.9%
En este caso, el cache redujo la latencia en un 97.5% y liberó recursos críticos de la base de datos, mejorando la escalabilidad general del sistema.

Errores comunes

  • TTL demasiado largo o infinito: Almacenar datos en cache sin expiración puede llevar a datos obsoletos. Solución: Define TTLs basados en la frecuencia de cambio de los datos (ej., 300 segundos para datos semi-estáticos).
  • Falta de invalidación: No actualizar o eliminar el cache cuando los datos cambian, causando inconsistencias. Solución: Invalida las claves relevantes en operaciones de escritura (ej., al actualizar un recurso).
  • Claves mal diseñadas: Usar claves genéricas que causan colisiones o dificultan la gestión. Solución: Usa un esquema jerárquico como tipo:id (ej., user:123).
  • Cache de respuestas con errores: Almacenar respuestas de error (ej., 404) puede llenar el cache con datos inútiles. Solución: Solo cachea respuestas exitosas (códigos 2xx).
  • Ignorar el costo de serialización: Serializar/deserializar JSON en cada operación de cache añade overhead. Solución: Usa formatos eficientes o considera bibliotecas optimizadas.

Checklist de dominio

  1. ¿Puedes configurar una conexión a Redis desde tu backend y manejar errores de conexión?
  2. ¿Sabes diseñar claves de cache únicas y descriptivas para diferentes recursos de tu API?
  3. ¿Implementas correctamente el patrón cache-aside en al menos un endpoint REST?
  4. ¿Defines TTLs apropiados basados en la naturaleza de los datos (ej., corto para datos dinámicos, largo para estáticos)?
  5. ¿Invalidas el cache automáticamente cuando los datos se actualizan o eliminan?
  6. ¿Mides el impacto del cache en métricas como latencia y carga de base de datos?
  7. ¿Manejas edge cases como fallos de Redis sin afectar la funcionalidad principal de la API?

Implementa Cache en un Endpoint de API para Lista de Productos

En este ejercicio, implementaras cache con Redis en un endpoint de API REST que devuelve una lista de productos. Sigue estos pasos:

  1. Configura un proyecto Node.js con Express y conecta a un servidor Redis local o en la nube. Usa la biblioteca ioredis para la conexion.
  2. Crea un endpoint GET /products que simule obtener una lista de productos desde una base de datos (puedes usar un array en memoria como simulacion).
  3. Implementa la logica de cache: Antes de devolver la lista, verifica si existe una clave products:list en Redis. Si existe, devuelve los datos cacheados; si no, obten los productos, almacenalos en Redis con un TTL de 60 segundos, y luego devuelvelos.
  4. Añade un endpoint POST /products para agregar un nuevo producto. En este endpoint, despues de agregar el producto, invalida la clave products:list en Redis para mantener la consistencia.
  5. Prueba tu implementacion: Realiza solicitudes GET para ver el cache en accion, y luego POST para ver como se invalida. Usa herramientas como curl o Postman.

Objetivo: Reducir el tiempo de respuesta del endpoint GET en solicitudes repetidas y asegurar que los datos siempre esten actualizados.

Pistas
  • Recuerda usar JSON.stringify() para almacenar objetos en Redis y JSON.parse() para recuperarlos.
  • Configura un manejador de errores para la conexion a Redis, para que tu API siga funcionando incluso si Redis falla.
  • Considera usar un patron de retry para la conexion a Redis en entornos de produccion.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.