Neste tutorial, vamos construir um serviço escalável de encurtamento de URL usando Node.js e Redis. Este serviço irá aproveitar o cache distribuído para lidar eficientemente com alto tráfego, reduzir a latência e escalar de forma transparente. Vamos explorar conceitos-chave como hashing consistente, estratégias de invalidação de cache e sharding 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 falhas 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.

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

  • Utilizando Docker para simular várias instâncias do Redis para sharding e escalabilidade.

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 encurtador de URL onde:

  1. Os 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 em várias instâncias do Redis para lidar com alto tráfego.

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

Arquitetura do Sistema

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

  1. Servidor de API: Lida com solicitações para encurtar e recuperar URLs.

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

  3. Docker: Simula um ambiente distribuído com múltiplos contêineres 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 iremos 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. Iremos utilizar essas instâncias para implementar hashing consistente e shard.

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

Vamos criar nosso arquivo principal da aplicação, 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 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:

    • Nós 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 equilibrada entre as instâncias do Redis.

  2. Encurtamento de URL:

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

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

  3. Redirecionamento de URL:

    • O ponto de extremidade /: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, URLs podem expirar ou mudar ao longo do tempo. Para lidar com isso, precisamos implementar 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 (tempo de vida) é 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 (Tempo de Vida): 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 é removida automaticamente do cache.

Passo 5: Monitoramento de Métricas de Cache

Para monitorar os hits e erros de cache, adicionaremos alguns registros 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:

  • Hits no Cache: Se uma URL for encontrada no cache, é um hit no cache.

  • Erros de Cache: Se uma URL não for encontrada, é um erro de cache.

  • Esses registros ajudarão 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://localhost: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ários instâncias do Redis.

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

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

  4. Monitore acertos e erros de cache para otimizar o desempenho.

Próximos Passos:

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

  • Implemente Análises: Acompanhe contagens de cliques e análises para URLs encurtadas.

  • Implante na Nuvem: Implante sua aplicação usando Kubernetes para dimensionamento automático e resiliência.

¡Feliz codificação!