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:
-
Usuários podem encurtar URLs longas e recuperar as URLs originais.
-
O serviço utiliza Redis caching para armazenar mapeamentos entre URLs encurtadas e URLs originais.
-
O cache é distribuído entre várias instâncias do Redis para lidar com alto tráfego.
-
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:
-
Servidor API: Gerencia solicitações de encurtar e recuperar URLs.
-
Camada de Cache Redis: Usa várias instâncias do Redis para caching distribuído.
-
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:
-
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.
-
-
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.
-
-
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
- Inicie suas instâncias do Redis:
docker start redis1 redis2 redis3
- Execute o servidor Node.js:
node index.js
-
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:
-
Implemente hashing consistente para distribuir entradas de cache em várias instâncias do Redis.
-
Otimizar sua aplicação com estratégias de invalidação de cache para manter os dados atualizados.
-
Usar Docker para simular um ambiente distribuído com múltiplos nós do Redis.
-
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!