Neste tutorial, vamos construir um serviço de encurtamento de URL escalável usando Node.js e Redis. Este serviço aproveitará o cache distribuído para lidar com alto tráfego de forma eficiente, reduzir a latência e escalar sem problemas. Vamos explorar conceitos-chave como hashing consistente, estratégias de invalidação de cache e shard para garantir que o sistema permaneça rápido e confiável.

Ao final deste guia, você terá um serviço de encurtamento de URL totalmente funcional que utiliza cache distribuído para otimizar o desempenho. Também criaremos uma demonstração interativa onde os usuários podem inserir URLs e ver métricas em tempo real como acertos e erros de cache.

O que Você Vai Aprender

  • Como construir um serviço de encurtamento de URL usando Node.js e Redis.

  • Como implementar cache distribuído para otimizar o desempenho.

  • Compreendendo o hashing consistente e estratégias de invalidação de cache.

  • Usando o Docker para simular múltiplas instâncias do Redis para shard e escala.

Pré-requisitos

Antes de começar, certifique-se de ter os seguintes itens instalados:

  • Node.js (v14 ou superior)

  • Redis

  • Docker

  • Conhecimento básico de JavaScript, Node.js e Redis.

Sumário

Visão Geral do Projeto

Vamos construir um serviço de encurtamento de URL onde:

  1. Usuários podem encurtar URLs longas e recuperar as URLs originais.

  2. O serviço utiliza Redis caching para armazenar mapeamentos entre URLs encurtadas e URLs originais.

  3. O cache é distribuído entre várias instâncias do Redis para lidar com alto tráfego.

  4. O sistema demonstrará hits de cache e erros de cache em tempo real.

Arquitetura do Sistema

Para garantir escalabilidade e desempenho, dividiremos nosso serviço nos seguintes componentes:

  1. Servidor API: Gerencia solicitações de encurtar e recuperar URLs.

  2. Camada de Cache Redis: Usa várias instâncias do Redis para caching distribuído.

  3. Docker: Simula um ambiente distribuído com múltiplos containers do Redis.

Passo 1: Configurando o Projeto

Vamos configurar nosso projeto inicializando uma aplicação Node.js:

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

Agora, instale as dependências necessárias:

npm install express redis shortid dotenv
  • express: Um framework de servidor web leve.

  • redis: Para lidar com caching.

  • shortid: Para gerar IDs curtos e únicos.

  • dotenv: Para gerenciar variáveis de ambiente.

Crie um arquivo .env na raiz do seu projeto:

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

Essas variáveis definem os hosts e portas do Redis que vamos utilizar.

Passo 2: Configurando Instâncias do Redis

Vamos usar o Docker para simular um ambiente distribuído com múltiplas instâncias do Redis.

Execute os seguintes comandos para iniciar três containers do 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

Isso configurará três instâncias do Redis em execução em portas diferentes. Vamos usar essas instâncias para implementar o hashing consistente e sharding.

Passo 3: Implementando o Serviço de Encurtamento de URL

Vamos criar nosso arquivo de aplicação 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 })
];

// Função de hash para distribuir chaves entre os clientes do Redis
function getRedisClient(key) {
  const hash = key.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
  return redisClients[hash % redisClients.length];
}

// Endpoint para encurtar uma 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}` });
});

// Endpoint para recuperar a 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 você pode ver neste código, temos:

  1. Hashing Consistente:

    • Distribuímos chaves (URLs encurtadas) entre vários clientes do Redis usando uma função de hash simples.

    • A função de hash garante que as URLs sejam distribuídas de forma equitativa entre as instâncias do Redis.

  2. Encurtamento de URL:

    • O endpoint /shorten aceita uma URL longa e gera um ID curto usando a biblioteca shortid.

    • A URL encurtada é armazenada em uma das instâncias do Redis usando nossa função de hash.

  3. Redirecionamento de URL:

    • O endpoint /:shortId recupera a URL original do cache e redireciona o usuário.

    • Se a URL não for encontrada no cache, uma resposta 404 é retornada.

Passo 4: Implementando Invalidez de Cache

Em uma aplicação do mundo real, as URLs podem expirar ou mudar com o tempo. Para lidar com isso, precisamos implementar a invalidez de cache.

Adicionando Expiração às URLs em Cache

Vamos modificar nosso arquivo index.js para definir um tempo de expiração para cada entrada em cache:

// Endpoint para encurtar uma URL com expiração
app.post('/shorten', async (req, res) => {
  const { url, ttl } = req.body; // ttl (time-to-live) é 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 padrão de 1 hora
  res.json({ shortUrl: `http://localhost:${process.env.PORT}/${shortId}` });
});
  • TTL (Time-To-Live): Definimos um tempo de expiração padrão de 1 hora para cada URL encurtada. Você pode personalizar o TTL para cada URL, se necessário.

  • Invalidez de Cache: Quando o TTL expira, a entrada é automaticamente removida do cache.

Passo 5: Monitorando Métricas de Cache

Para monitorar acertos de cache e faltas de cache, vamos adicionar alguns logs aos nossos endpoints em 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);
  });
});

Aqui está o que está acontecendo neste código:

  • Acertos de Cache: Se uma URL for encontrada no cache, é um acerto de cache.

  • Faltas de Cache: Se uma URL não for encontrada, é uma falta de cache.

  • Esse logging ajudará você a monitorar o desempenho do seu cache distribuído.

Passo 6: Testando a Aplicação

  1. Inicie suas instâncias do Redis:
docker start redis1 redis2 redis3
  1. Execute o servidor Node.js:
node index.js
  1. Teste os endpoints usando curl ou Postman:

    • Encurtar uma URL:

        POST http://localhost:3000//shorten
        Corpo: { "url": "https://example.com" }
      
    • Acessar a URL encurtada:

        GET http::3000//{shortId}
      

Conclusão: O Que Você Aprendeu

Parabéns! Você construiu com sucesso um serviço de encurtamento de URL escalável com cache distribuído usando Node.js e Redis. Ao longo deste tutorial, você aprendeu como:

  1. Implemente hashing consistente para distribuir entradas de cache em várias instâncias do Redis.

  2. Otimizar sua aplicação com estratégias de invalidação de cache para manter os dados atualizados.

  3. Usar Docker para simular um ambiente distribuído com múltiplos nós do Redis.

  4. Monitorar acertos e falhas de cache para otimizar o desempenho.

Próximos Passos:

  • Adicionar um Banco de Dados: Armazenar URLs em um banco de dados para persistência além do cache.

  • Implementar Análises: Acompanhar contagens de cliques e análises para URLs encurtadas.

  • Implantar na Nuvem: Implemente sua aplicação usando Kubernetes para escalabilidade automática e resiliência.

Boa codificação!