Cache Distribuído com Redis e Node.js: Guia Prático de Implementação
Introdução
Em aplicações web modernas, a latência de acesso a dados pode ser o principal gargalo de desempenho. Quando múltiplas instâncias de um serviço precisam compartilhar informações rapidamente, o cache distribuído surge como solução eficaz. Neste artigo, vamos explorar como implementar um cache distribuído usando Redis como camada de armazenamento em memória e Node.js como runtime de aplicação.
Entenderemos os conceitos fundamentais, configuraremos o Redis em ambiente de desenvolvimento (Docker), integraremos ao Express e aplicaremos estratégias de expiração e invalidação. Ao final, você terá um código pronto para produção que reduz drasticamente o tempo de resposta de consultas frequentes.
Desenvolvimento
1. Conceitos de Cache Distribuído
- Cache local vs. distribuído: o cache local (em memória da própria aplicação) é rápido, mas não compartilha dados entre instâncias. O cache distribuído, como o Redis, mantém um único ponto de verdade acessível por todas as réplicas.
- Consistência: em sistemas de alta escala, a consistência eventual costuma ser suficiente; o Redis oferece operações atômicas que ajudam a manter a coerência.
- TTL (Time‑To‑Live): define quanto tempo um registro permanece válido no cache, evitando dados obsoletos.
2. Configurando o Redis
A forma mais simples de iniciar um servidor Redis para desenvolvimento é via Docker:
bash
docker-compose.yml
version: "3.8" services: redis: image: redis:7-alpine container_name: redis-cache ports: - "6379:6379" restart: unless-stopped command: ["redis-server", "--appendonly", "yes"]
Execute:
bash docker compose up -d
O Redis ficará disponível em localhost:6379. Você pode testar a conexão com o cliente CLI:
bash redis-cli ping
Resposta: PONG
3. Integrando Redis ao Node.js
Usaremos o pacote ioredis, que oferece suporte a cluster e reconexão automática.
bash npm install express ioredis
3.1. Criação da camada de cache
javascript // cache.js const Redis = require('ioredis'); const redis = new Redis({ host: process.env.REDIS_HOST || '127.0.0.1', port: process.env.REDIS_PORT || 6379, });
/**
- Obtém um valor do cache ou executa a função de fallback.
- @param {string} key Chave única para o item.
- @param {function} fetchFn Função async que busca o dado na origem.
- @param {number} ttl Tempo em segundos que o item ficará no cache. */ async function getOrSet(key, fetchFn, ttl = 300) { const cached = await redis.get(key); if (cached) { return JSON.parse(cached); } const fresh = await fetchFn(); await redis.set(key, JSON.stringify(fresh), 'EX', ttl); return fresh; }
module.exports = { getOrSet, redis };
3.2. Middleware de cache para rotas Express
javascript // middleware/cacheMiddleware.js const { getOrSet } = require('../cache');
function cache(ttl) {
return async (req, res, next) => {
const key = __express__${req.originalUrl};
try {
const data = await getOrSet(key, async () => null, ttl);
if (data) {
return res.json(data);
}
// Se não houver cache, delega ao próximo handler
res.locals.cacheKey = key; // salva a chave para uso posterior
next();
} catch (err) {
next(err);
}
};
}
module.exports = cache;
3.3. Exemplo de API usando o cache
javascript // index.js const express = require('express'); const axios = require('axios'); const cache = require('./middleware/cacheMiddleware'); const { redis } = require('./cache');
const app = express(); const PORT = process.env.PORT || 3000;
// Rota que consome um endpoint externo (ex.: JSONPlaceholder)
app.get('/posts/:id', cache(120), async (req, res, next) => {
const { id } = req.params;
try {
const response = await axios.get(https://jsonplaceholder.typicode.com/posts/${id});
const result = response.data;
// Salva no cache antes de responder
await redis.set(res.locals.cacheKey, JSON.stringify(result), 'EX', 120);
res.json(result);
} catch (err) {
next(err);
}
});
app.listen(PORT, () => {
console.log(🚀 API rodando na porta ${PORT});
});
Neste fluxo, a primeira requisição a /posts/1 busca o dado externo, armazena no Redis por 2 minutos e devolve ao cliente. As chamadas subsequentes dentro desse intervalo retornam imediatamente do cache.
4. Estratégias de Expiração e Invalidação
- TTL curto para dados voláteis (ex.: sessões, contadores). Use valores de 30‑300 s.
- TTL longo para catálogos estáticos (ex.: lista de produtos). Pode chegar a 24 h.
- Cache‑aside (lazy loading): como no exemplo acima, o dado só entra no cache quando solicitado.
- Write‑through: ao atualizar a fonte de dados, escreva simultaneamente no Redis para evitar stale reads.
- Invalidar por evento: ao receber um webhook de atualização, chame
redis.del(key).
javascript
// Exemplo de invalidação ao atualizar um produto
app.put('/products/:id', async (req, res, next) => {
const { id } = req.params;
const payload = req.body;
// ... lógica de atualização no DB
await redis.del(product:${id}); // remove versão antiga do cache
res.json({ status: 'atualizado' });
});
Exemplos Práticos
1. Deploy do Redis em produção (AWS Elasticache)
Embora o Docker seja ótimo para desenvolvimento, em produção recomenda‑se um serviço gerenciado. No AWS Elasticache, basta criar um cluster Redis, habilitar Encryption in‑Transit e apontar a aplicação para o endpoint fornecido.
javascript // Configuração de produção (env) const redis = new Redis({ host: process.env.REDIS_ENDPOINT, // ex.: mycluster.xxxxxx.ng.0001.use1.cache.amazonaws.com port: 6379, tls: {}, // ativa TLS });
2. Monitoramento com redis-cli e INFO
bash
Ver estatísticas de uso
redis-cli INFO memory
Limpar todo o cache (cuidado!)
redis-cli FLUSHALL
3. Teste de carga com k6
javascript import http from 'k6/http'; import { check, sleep } from 'k6';
export const options = { stages: [{ duration: '30s', target: 100 }], // 100 VUs };
export default function () { const res = http.get('http://localhost:3000/posts/1'); check(res, { 'status 200': (r) => r.status === 200 }); sleep(1); }
A diferença de latência entre a primeira (hit‑miss) e as seguintes (hit) costuma ser de ~200 ms para a origem versus <5 ms para o Redis.
Conclusão
Implementar um cache distribuído com Redis e Node.js traz ganhos de desempenho palpáveis, especialmente em APIs que consultam fontes externas ou bancos de dados relacionalmente pesados. Seguindo as boas práticas de TTL, invalidação baseada em eventos e monitoramento constante, é possível manter a consistência dos dados sem sacrificar a velocidade.
Próximos passos:
- Avaliar a necessidade de um cluster Redis para alta disponibilidade.
- Integrar métricas ao Prometheus/Grafana.
- Explorar recursos avançados como Redis Streams ou Lua scripting para lógica de cache mais sofisticada.
Com a base apresentada, sua aplicação está pronta para escalar de forma eficiente e resiliente.



