Cache é uma das técnicas mais poderosas para melhorar performance. Ao armazenar resultados de operações caras, evitamos repetir trabalho e reduzimos latência dramaticamente.
Mas cache não é mágica. Usado incorretamente, ele cria problemas sutis, difíceis de debugar e potencialmente graves. Este artigo explora os benefícios do cache, suas armadilhas, e como usá-lo de forma efetiva.
Cache é como sal na comida: a quantidade certa melhora tudo, demais estraga.
Por que Cache Funciona
O princípio da localidade
A maioria dos sistemas exibe padrões previsíveis:
- Localidade temporal: dados acessados recentemente serão acessados novamente
- Localidade espacial: dados próximos aos acessados também serão acessados
Se 80% das requisições acessam 20% dos dados (Pareto), cachear esses 20% tem impacto enorme.
Redução de latência
Sem cache: App → Banco de dados → Resposta
10ms + 50ms = 60ms
Com cache: App → Cache hit → Resposta
10ms + 1ms = 11ms
Melhoria de 5x ou mais é comum.
Redução de carga
Cada cache hit é uma requisição que não vai para:
- Banco de dados
- API externa
- Processamento pesado
Níveis de Cache
1. Cache de CPU (L1, L2, L3)
Gerenciado pelo hardware. Você não controla diretamente, mas pode escrever código cache-friendly.
2. Cache de aplicação (in-memory)
const cache = new Map();
function getUser(id) {
if (cache.has(id)) return cache.get(id);
const user = fetchFromDB(id);
cache.set(id, user);
return user;
}
Prós: extremamente rápido, simples Contras: não compartilhado entre instâncias, perdido em restart
3. Cache distribuído (Redis, Memcached)
Compartilhado entre instâncias da aplicação.
Prós: compartilhado, persistente (com configuração), escalável Contras: latência de rede, mais complexo
4. Cache de CDN
Para conteúdo estático e páginas cacheáveis.
Prós: distribuído geograficamente, reduz carga no origin Contras: invalidação complexa
5. Cache de banco de dados
Query cache, buffer pool, etc. Gerenciado pelo banco.
As Armadilhas do Cache
1. Cache stampede (thundering herd)
Problema: quando uma entrada expira, múltiplas requisições simultâneas tentam recalculá-la.
Cache expira
↓
100 requisições chegam
↓
Todas vão para o banco
↓
Banco sobrecarregado
Soluções:
- Locking (apenas um recalcula, outros esperam)
- Refresh antecipado (antes de expirar)
- Probabilistic early expiration
2. Dados desatualizados (stale data)
Problema: cache mostra dados que já mudaram na fonte.
Soluções:
- TTL adequado ao caso de uso
- Invalidação explícita em writes
- Cache-aside pattern com invalidação
3. Cache inconsistente entre instâncias
Problema: cache local diferente em cada servidor.
Servidor A: user.name = "João"
Servidor B: user.name = "Maria" (atualizado)
Soluções:
- Usar cache distribuído
- TTL curto para dados voláteis
- Pub/sub para invalidação
4. Memory pressure
Problema: cache cresce até consumir toda memória.
Soluções:
- Limitar tamanho do cache
- Política de evicção (LRU, LFU)
- Monitorar uso de memória
5. Cache de dados que não deveria cachear
Problema: cachear dados sensíveis ou específicos de usuário incorretamente.
Riscos:
- Vazamento de dados entre usuários
- Problemas de LGPD/GDPR
- Comportamento incorreto
6. Cold start
Problema: após deploy ou restart, cache está vazio.
Soluções:
- Warm-up proativo
- Degradação graceful
- Rolling deploys
Estratégias de Cache
Cache-Aside (Lazy Loading)
function getData(key) {
let data = cache.get(key);
if (!data) {
data = database.get(key);
cache.set(key, data, ttl);
}
return data;
}
Prós: simples, dados cacheados sob demanda Contras: primeira requisição sempre lenta
Write-Through
function saveData(key, data) {
database.save(key, data);
cache.set(key, data, ttl);
}
Prós: cache sempre atualizado Contras: writes mais lentos
Write-Behind (Write-Back)
function saveData(key, data) {
cache.set(key, data);
// Persiste assincronamente depois
queue.push({ key, data });
}
Prós: writes muito rápidos Contras: risco de perda de dados, complexo
Refresh-Ahead
Cache é atualizado automaticamente antes de expirar.
Prós: evita cache miss, latência consistente Contras: pode atualizar dados não acessados
Métricas Essenciais
Hit rate
Hit rate = cache hits / (cache hits + cache misses)
Bom: > 90% Ótimo: > 95% Ruim: < 80% (reconsidere a estratégia)
Latência de cache
Cache muito lento pode não valer a pena. Meça:
- Tempo de leitura
- Tempo de escrita
- Latência de rede (distribuído)
Eviction rate
Muitas evições indicam cache pequeno demais ou TTL muito longo.
Boas Práticas
1. Defina TTL apropriado
| Tipo de dado | TTL sugerido |
|---|---|
| Configuração | 5-15 minutos |
| Dados de catálogo | 1-5 minutos |
| Sessão de usuário | 30 minutos |
| Dados calculados | Depende da frequência de mudança |
2. Use keys estruturadas
user:123:profile
product:456:inventory
search:hash(query)
3. Serialize eficientemente
JSON é legível mas lento. Para alto volume, considere:
- MessagePack
- Protocol Buffers
- Avro
4. Monitore sempre
- Hit rate por tipo de cache
- Latência
- Uso de memória
- Evictions
5. Tenha fallback
try {
return cache.get(key);
} catch (e) {
// Cache indisponível, vai direto na fonte
return database.get(key);
}
Quando NÃO Usar Cache
- Dados que mudam a cada requisição
- Dados muito grandes (custo de serialização)
- Hit rate esperado muito baixo
- Quando consistência forte é mandatória
- Quando a fonte já é rápida o suficiente
Conclusão
Cache é uma ferramenta poderosa, mas requer cuidado:
- Entenda seus padrões de acesso antes de cachear
- Escolha a estratégia certa para cada caso
- Configure TTL adequado — nem muito curto nem muito longo
- Prepare-se para falhas — cache indisponível não pode derrubar o sistema
- Monitore continuamente — hit rate, latência, memória
Usado corretamente, cache pode transformar um sistema lento em um sistema rápido. Usado incorretamente, pode criar bugs sutis e problemas de escala.
Cache é uma troca: você troca consistência por velocidade. Certifique-se de que a troca vale a pena.