Cache em camadas: browser, CDN, aplicação e banco de dados
1. Introdução à arquitetura de cache em camadas
Cache não é bala de prata. Em sistemas distribuídos, a latência de rede, o custo de armazenamento e a complexidade de consistência tornam o design de cache um dos desafios mais sutis da engenharia de software. A arquitetura de cache em camadas busca endereçar esses problemas explorando o princípio da localidade: dados acessados recentemente tendem a ser acessados novamente, e dados próximos no espaço de endereçamento também.
Cada camada opera em uma faixa de velocidade distinta:
- Browser cache: milissegundos (memória local)
- CDN: dezenas de milissegundos (rede de borda)
- Aplicação: microssegundos a milissegundos (RAM)
- Banco de dados: submilissegundos (buffer pool)
A hierarquia é clara: quanto mais próximo do usuário, mais rápido e mais caro por byte. O desafio é decidir o que armazenar em cada nível.
2. Cache no browser: a primeira linha de defesa
O navegador é a camada mais próxima do usuário e a mais barata — não custa nada para o servidor. O HTTP caching é a base:
Cache-Control: public, max-age=3600, immutable
ETag: "abc123"
Last-Modified: Tue, 15 Mar 2025 12:00:00 GMT
O cabeçalho immutable indica que o recurso não muda entre versões, evitando revalidações desnecessárias. Para recursos dinâmicos, a estratégia stale-while-revalidate permite servir conteúdo envelhecido enquanto atualiza em segundo plano:
Cache-Control: max-age=300, stale-while-revalidate=600
Service Workers ampliam esse controle com a Cache API:
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(response => {
caches.put(event.request, response.clone());
return response;
});
return cached || fetchPromise;
})
);
});
Armadilha comum: versionar assets com hash no nome do arquivo (app.abc123.js) evita que o browser sirva versões antigas após deploy.
3. CDN: distribuindo conteúdo globalmente
CDNs (Content Delivery Networks) colocam servidores de borda próximos geograficamente aos usuários. O fluxo típico:
- Usuário solicita
https://cdn.exemplo.com/img/foto.jpg - DNS resolve para o edge server mais próximo
- Se cache hit, serve imediatamente
- Se cache miss, busca no origin server e armazena
As zonas de cache podem ser pull (o CDN busca do origin sob demanda) ou push (você envia os arquivos antecipadamente). A invalidação é crítica:
# Purga por URL
curl -X POST https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"files":["https://exemplo.com/img/foto.jpg"]}'
# Purga por tag
Cache-Tag: banner-principal, promocao-2025
Edge computing (Cloudflare Workers, Lambda@Edge) permite cache de conteúdo dinâmico:
// Cloudflare Worker: cache de API com TTL variável
async function handleRequest(request) {
const url = new URL(request.url);
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
response = await fetch(request);
const ttl = url.pathname.startsWith('/api/status') ? 10 : 3600;
response = new Response(response.body, response);
response.headers.set('Cache-Control', `public, max-age=${ttl}`);
cache.put(cacheKey, response.clone());
}
return response;
}
4. Cache na aplicação: memória, Redis e padrões
A aplicação é onde o controle fino acontece. Cache em memória local (in-process) é rápido mas não escala entre instâncias. Cache distribuído (Redis, Memcached) resolve isso.
Padrões comuns:
Cache-Aside (Lazy Loading):
function getUser(id):
key = "user:" + id
user = cache.get(key)
if user is None:
user = database.query("SELECT * FROM users WHERE id = ?", id)
cache.set(key, user, ttl=3600)
return user
Read-Through (cache gerencia a busca):
cache.get("user:123", callback=lambda: database.query(...))
Write-Through (atualiza cache e banco simultaneamente):
function updateUser(id, data):
database.update("users", data, where="id = ?", id)
cache.set("user:" + id, data)
Write-Behind (atualiza cache imediatamente, banco assíncrono):
function updateUser(id, data):
cache.set("user:" + id, data)
queue.enqueue(lambda: database.update(...))
Políticas de evicção: LRU (menos recentemente usado) para padrão, LFU (menos frequente) para hotspots, FIFO para filas. TTLs devem ser baseados na natureza dos dados:
# Chave composta para cache de feed
key = f"feed:{user_id}:{page}:{timestamp}"
cache.set(key, feed_data, ttl=300) # 5 minutos
5. Cache no banco de dados: buffer pool e query cache
O banco de dados gerencia seu próprio cache no buffer pool (MySQL InnoDB) ou shared buffers (PostgreSQL). Páginas de dados e índices são mantidas em RAM para acesso rápido:
# MySQL: verificar tamanho do buffer pool
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
# Saída: 134217728 (128 MB)
# PostgreSQL: verificar shared buffers
SHOW shared_buffers;
# Saída: 128MB
O query cache (MySQL) armazena resultados de consultas SELECT idênticas. Porém, em tabelas com alta taxa de escrita, o overhead de invalidação supera o ganho:
# Desabilitar query cache em MySQL 8.0+
query_cache_type = 0
query_cache_size = 0
Materialized Views funcionam como cache estrutural no PostgreSQL:
CREATE MATERIALIZED VIEW daily_sales AS
SELECT DATE(created_at), SUM(amount)
FROM orders
GROUP BY DATE(created_at);
REFRESH MATERIALIZED VIEW daily_sales; -- Atualização manual
Índices também são uma forma de cache — mantêm dados ordenados em RAM para buscas rápidas.
6. Consistência entre camadas: o problema do cache stampede
O paradoxo de “dois programadores” (invalidação de cache é um dos dois problemas difíceis da computação) se manifesta no cache stampede: quando um item expira e milhares de requisições simultâneas batem no banco.
Mitigações:
Lock distribuído (Redis Redlock):
function getData(key):
data = cache.get(key)
if data is None:
lock = acquire_lock("lock:" + key, ttl=5)
if lock:
data = database.query(...)
cache.set(key, data, ttl=3600)
release_lock(lock)
else:
sleep(0.1)
return getData(key) # retry
return data
Backoff exponencial:
for attempt in range(3):
data = cache.get(key)
if data: break
sleep(0.1 * (2 ** attempt))
data = database.query(...)
Cache warming: pré-carregar dados críticos após deploy:
for user in active_users:
cache.set(f"profile:{user.id}", generate_profile(user), ttl=3600)
7. Monitoramento e métricas de eficiência
Métricas essenciais:
| Métrica | Fórmula | Interpretação |
|---|---|---|
| Hit ratio | hits / (hits + misses) | >90% é excelente |
| Miss rate | misses / total | >10% indica problema |
| Latência média | soma tempos / total | Comparar com e sem cache |
| Taxa de expiração | expirações / total | Ajustar TTL |
Ferramentas:
# Redis: monitorar hit ratio
redis-cli INFO stats | grep keyspace_hits
keyspace_hits:1234567
keyspace_misses:12345
# OpenTelemetry: tracing de cache
span.set_attribute("cache.hit", true)
span.set_attribute("cache.key", key)
# Grafana: dashboard com painéis de hit ratio por camada
Depuração: simular carga com wrk ou k6 e monitorar picos de latência. Se o hit ratio cai abruptamente, ajuste TTLs ou verifique invalidação em cascata.
8. Conclusão e boas práticas para projetos reais
Checklist para decisão:
| Camada | O que cachear | O que não cachear |
|---|---|---|
| Browser | Assets estáticos, imagens, CSS/JS | Dados sensíveis, conteúdo personalizado |
| CDN | Conteúdo público, APIs GET | POST, dados de usuário |
| Aplicação | Resultados de queries, sessões | Dados voláteis, transações |
| Banco | Páginas quentes, índices | Tabelas inteiras sem critério |
Trade-offs finais:
- Cache em mais camadas aumenta complexidade de invalidação
- Cache em menos camadas aumenta latência para o usuário
- Custo de infraestrutura (RAM, CDN) vs. ganho de performance
A regra de ouro: nunca otimize sem medir. Implemente cache apenas onde o gargalo for confirmado por métricas.
Referências
- HTTP Caching (MDN Web Docs) — Documentação completa sobre cabeçalhos de cache HTTP, estratégias e boas práticas.
- Cloudflare Cache Documentation — Guia oficial sobre configuração de cache, purga e edge computing com Cloudflare Workers.
- Redis Cache-Aside Pattern (Redis Docs) — Tutorial prático sobre o padrão Cache-Aside com Redis, incluindo exemplos de código.
- MySQL InnoDB Buffer Pool (MySQL Documentation) — Explicação detalhada do buffer pool, configuração e monitoramento.
- Cache Stampede Prevention (AWS Architecture Blog) — Estratégias para evitar cache stampede usando locks distribuídos e backoff exponencial.
- OpenTelemetry Cache Tracing — Como instrumentar operações de cache com OpenTelemetry para tracing distribuído.
- PostgreSQL Materialized Views (PostgreSQL Docs) — Documentação oficial sobre materialized views como cache estrutural.