En este tutorial, construiremos un servicio escalable de acortamiento de URL utilizando Node.js y Redis. Este servicio aprovechará el almacenamiento en caché distribuido para manejar eficientemente un alto tráfico, reducir la latencia y escalar sin problemas. Exploraremos conceptos clave como el hashing consistente, estrategias de invalidación de caché y fragmentación para asegurar que el sistema permanezca rápido y confiable.

Al final de esta guía, tendrás un servicio de acortamiento de URL completamente funcional que utiliza almacenamiento en caché distribuido para optimizar el rendimiento. También crearemos una demo interactiva donde los usuarios pueden ingresar URLs y ver métricas en tiempo real como aciertos y fallos de caché.

Lo Que Aprenderás

  • Cómo construir un servicio de acortamiento de URL utilizando Node.js y Redis.

  • Cómo implementar almacenamiento en caché distribuido para optimizar el rendimiento.

  • Comprender el hashing consistente y las estrategias de invalidación de caché.

  • Usar Docker para simular múltiples instancias de Redis para fragmentación y escalabilidad.

Prerrequisitos

Antes de comenzar, asegúrate de tener instalado lo siguiente:

  • Node.js (v14 o superior)

  • Redis

  • Docker

  • Conocimientos básicos de JavaScript, Node.js y Redis.

Tabla de Contenidos

Resumen del Proyecto

Construiremos un servicio de acortamiento de URL donde:

  1. Los usuarios pueden acortar URLs largas y recuperar las URLs originales.

  2. El servicio utiliza el almacenamiento en caché de Redis para almacenar las correspondencias entre URLs acortadas y URLs originales.

  3. La caché está distribuida en múltiples instancias de Redis para manejar el alto tráfico.

  4. El sistema mostrará hits de caché y misses en tiempo real.

Arquitectura del Sistema

Para garantizar escalabilidad y rendimiento, dividiremos nuestro servicio en los siguientes componentes:

  1. Servidor de API: Maneja solicitudes de acortamiento y recuperación de URLs.

  2. Capa de Caché Redis: Utiliza múltiples instancias de Redis para caché distribuida.

  3. Docker: Simula un entorno distribuido con múltiples contenedores de Redis.

Paso 1: Configuración del Proyecto

Configuremos nuestro proyecto inicializando una aplicación Node.js:

mkdir scalable-url-shortener
cd scalable-url-shortener
npm init -y

Ahora, instalemos las dependencias necesarias:

npm install express redis shortid dotenv
  • express: Un marco de servidor web ligero.

  • redis: Para manejar la caché.

  • shortid: Para generar IDs cortos y únicos.

  • dotenv: Para gestionar variables de entorno.

Crea un archivo .env en la raíz de tu proyecto:

PORT=3000
REDIS_HOST_1=localhost
REDIS_PORT_1=6379
REDIS_HOST_2=localhost
REDIS_PORT_2=6380
REDIS_HOST_3=localhost
REDIS_PORT_3=6381

Estas variables definen los hosts y puertos de Redis que vamos a utilizar.

Paso 2: Configuración de Instancias de Redis

Vamos a usar Docker para simular un entorno distribuido con múltiples instancias de Redis.

Ejecuta los siguientes comandos para iniciar tres contenedores de Redis:

docker run -p 6379:6379 --name redis1 -d redis
docker run -p 6380:6379 --name redis2 -d redis
docker run -p 6381:6379 --name redis3 -d redis

Esto configurará tres instancias de Redis ejecutándose en puertos diferentes. Utilizaremos estas instancias para implementar hashing consistente y particionamiento.

Paso 3: Implementación del Servicio de Acortamiento de URL

Creemos nuestro archivo de aplicación principal, index.js:

require('dotenv').config();
const express = require('express');
const redis = require('redis');
const shortid = require('shortid');

const app = express();
app.use(express.json());

const redisClients = [
  redis.createClient({ host: process.env.REDIS_HOST_1, port: process.env.REDIS_PORT_1 }),
  redis.createClient({ host: process.env.REDIS_HOST_2, port: process.env.REDIS_PORT_2 }),
  redis.createClient({ host: process.env.REDIS_HOST_3, port: process.env.REDIS_PORT_3 })
];

// Función hash para distribuir claves entre clientes de Redis
function getRedisClient(key) {
  const hash = key.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
  return redisClients[hash % redisClients.length];
}

// Punto final para acortar una URL
app.post('/shorten', async (req, res) => {
  const { url } = req.body;
  if (!url) return res.status(400).send('URL is required');

  const shortId = shortid.generate();
  const redisClient = getRedisClient(shortId);

  await redisClient.set(shortId, url);
  res.json({ shortUrl: `http://localhost:${process.env.PORT}/${shortId}` });
});

// Punto final para recuperar la URL original
app.get('/:shortId', async (req, res) => {
  const { shortId } = req.params;
  const redisClient = getRedisClient(shortId);

  redisClient.get(shortId, (err, url) => {
    if (err || !url) {
      return res.status(404).send('URL not found');
    }
    res.redirect(url);
  });
});

app.listen(process.env.PORT, () => {
  console.log(`Server running on port ${process.env.PORT}`);
});

Como puedes ver en este código, tenemos:

  1. Hashing Consistente:

    • Distribuimos claves (URLs acortadas) entre varios clientes de Redis usando una función hash simple.

    • La función hash asegura que las URLs se distribuyan de manera uniforme entre las instancias de Redis.

  2. Acortamiento de URL:

    • El punto de conexión /shorten acepta una URL larga y genera un ID corto utilizando la librería shortid.

    • La URL acortada se almacena en una de las instancias de Redis utilizando nuestra función hash.

  3. Redirección de URL:

    • El punto de conexión /:shortId recupera la URL original de la caché y redirige al usuario.

    • Si la URL no se encuentra en la caché, se devuelve una respuesta 404.

Paso 4: Implementación de la Invalidez de la Caché

En una aplicación del mundo real, las URL pueden caducar o cambiar con el tiempo. Para manejar esto, necesitamos implementar la invalidez de la caché.

Agregando Vencimiento a las URLs en Caché

Vamos a modificar nuestro archivo index.js para establecer un tiempo de vencimiento para cada entrada en caché:

// Endpoint para acortar una URL con vencimiento
app.post('/shorten', async (req, res) => {
  const { url, ttl } = req.body; // ttl (tiempo de vida) es opcional
  if (!url) return res.status(400).send('URL is required');

  const shortId = shortid.generate();
  const redisClient = getRedisClient(shortId);

  await redisClient.set(shortId, url, 'EX', ttl || 3600); // TTL predeterminado de 1 hora
  res.json({ shortUrl: `http://localhost:${process.env.PORT}/${shortId}` });
});
  • TTL (Tiempo de Vida): Establecemos un tiempo de vencimiento predeterminado de 1 hora para cada URL acortada. Puede personalizar el TTL para cada URL si es necesario.

  • Invalidez de la Caché: Cuando el TTL caduca, la entrada se elimina automáticamente de la caché.

Paso 5: Monitoreo de Métricas de la Caché

Para monitorear los aciertos de caché y errores, agregaremos un registro a nuestros puntos finales en index.js:

app.get('/:shortId', async (req, res) => {
  const { shortId } = req.params;
  const redisClient = getRedisClient(shortId);

  redisClient.get(shortId, (err, url) => {
    if (err || !url) {
      console.log(`Cache miss for key: ${shortId}`);
      return res.status(404).send('URL not found');
    }
    console.log(`Cache hit for key: ${shortId}`);
    res.redirect(url);
  });
});

Esto es lo que sucede en este código:

  • Aciertos de caché: Si se encuentra una URL en la caché, es un acierto de caché.

  • Errores de caché: Si una URL no se encuentra, es un error de caché.

  • Este registro te ayudará a monitorear el rendimiento de tu caché distribuida.

Paso 6: Probando la Aplicación

  1. Inicia tus instancias de Redis:
docker start redis1 redis2 redis3
  1. Ejecuta el servidor Node.js:
node index.js
  1. Prueba los puntos finales usando curl o Postman:

    • Acortar una URL:

        POST http://localhost:3000/shorten
        Cuerpo: "url": "https://example.com" }
      
    • Acceder a la URL acortada:

        GET http://localhost:3000/{shortId}
      

Conclusión: Lo que has aprendido

¡Felicidades! Has construido con éxito un servicio de acortador de URL escalable con caché distribuida utilizando Node.js y Redis. A lo largo de este tutorial, has aprendido cómo:

  1. Implementa hashing consistente para distribuir las entradas de caché en múltiples instancias de Redis.

  2. Optimiza tu aplicación con estrategias de invalidación de caché para mantener los datos actualizados.

  3. Utiliza Docker para simular un entorno distribuido con múltiples nodos de Redis.

  4. Monitorea aciertos y fallos de caché para optimizar el rendimiento.

Siguientes pasos:

  • Agregar una Base de Datos: Almacena URLs en una base de datos para una persistencia más allá de la caché.

  • Implementar Análisis: Seguir recuentos de clics y análisis para URLs acortadas.

  • Desplegar en la Nube: Implementa tu aplicación usando Kubernetes para autoescalado y resiliencia.

¡Feliz codificación!