Cache: estratégias, invalidação e onde colocar
1. Fundamentos de Cache na Arquitetura de Software
1.1. O que é cache e por que ele é crítico para desempenho
Cache é um componente de hardware ou software que armazena dados temporariamente para que requisições futuras possam ser atendidas mais rapidamente. Em arquitetura de software, o cache é uma das técnicas mais eficazes para reduzir latência, diminuir carga em bancos de dados e melhorar a experiência do usuário. Sem cache, cada requisição precisaria percorrer toda a cadeia de processamento, desde o cliente até o banco de dados, resultando em tempos de resposta elevados e maior consumo de recursos.
1.2. Latência vs. throughput: o impacto de um hit vs. miss
O desempenho do cache é medido principalmente pela taxa de acertos (hit rate). Um cache hit pode ser servido em microssegundos (se em memória), enquanto um cache miss pode custar dezenas de milissegundos até segundos, dependendo da fonte de dados. A relação entre hit e miss impacta diretamente o throughput do sistema: quanto maior o hit rate, mais requisições podem ser processadas por segundo com os mesmos recursos.
1.3. Hierarquia de cache
A hierarquia de cache pode ser organizada em múltiplos níveis:
text
Nível 1 (L1) - Cache local da CPU (nanossegundos)
Nível 2 (L2) - Cache local do servidor de aplicação (microssegundos)
Nível 3 (L3) - Cache distribuído como Redis/Memcached (milissegundos)
Nível 4 (L4) - CDN ou cache de borda (dezenas de milissegundos)
Cada nível oferece um trade-off entre velocidade, capacidade e custo.
2. Onde Colocar o Cache: Camadas e Topologias
2.1. Cache no cliente
Headers HTTP como Cache-Control e ETag permitem que navegadores e proxies armazenem respostas localmente. Service Workers podem implementar estratégias como "stale-while-revalidate" para servir conteúdo do cache enquanto atualizam em segundo plano.
text
Cache-Control: public, max-age=3600, stale-while-revalidate=300
ETag: "abc123"
2.2. Cache no servidor de aplicação
Soluções como Redis e Memcached oferecem cache distribuído de baixa latência. A escolha entre cache local (in-memory) e distribuído depende da necessidade de consistência entre instâncias:
text
# Cache local (in-memory) - HashMap simples
cache_local = {}
cache_local["usuario:123"] = {"nome": "João", "email": "joao@exemplo.com"}
# Cache distribuído - Redis
redis.setex("usuario:123", 3600, dados_usuario)
2.3. Cache na borda da rede
CDNs (Cloudflare, Akamai) e reverse proxies (Varnish, Nginx) armazenam respostas HTTP em pontos de presença geograficamente distribuídos, reduzindo latência para usuários finais e descarregando servidores de origem.
3. Estratégias de Cache: Padrões de Leitura e Escrita
3.1. Cache-Aside (Lazy Loading)
A aplicação verifica o cache antes de consultar a fonte de dados. Se não encontrar, busca no banco e armazena no cache.
text
function buscarUsuario(id):
dados = cache.get("usuario:" + id)
if dados is None:
dados = banco.buscarUsuario(id)
cache.set("usuario:" + id, dados, ttl=3600)
return dados
3.2. Read-Through e Write-Through
No Read-Through, o cache consulta automaticamente a fonte de dados em caso de miss. No Write-Through, toda escrita passa primeiro pelo cache, que atualiza o banco de dados sincronamente.
text
# Write-Through
function atualizarUsuario(id, novos_dados):
cache.set("usuario:" + id, novos_dados)
banco.atualizarUsuario(id, novos_dados)
3.3. Write-Behind (Write-Back) e Write-Around
Write-Behind acumula escritas no cache e persiste no banco assincronamente, melhorando throughput mas arriscando perda de dados. Write-Around escreve diretamente no banco, invalidando o cache apenas quando necessário.
4. Políticas de Substituição e Expiração
4.1. TTL (Time-To-Live)
O TTL define por quanto tempo um item permanece no cache. Valores típicos variam de segundos (dados voláteis) a horas (dados estáveis). A escolha do TTL é um equilíbrio entre frescor dos dados e hit rate.
text
# Redis com TTL
redis.setex("sessao:token_abc", 1800, dados_sessao)
4.2. Algoritmos de evicção
- LRU (Least Recently Used): Remove itens menos acessados recentemente. Ideal para padrões de acesso com localidade temporal.
- LFU (Least Frequently Used): Remove itens menos acessados no total. Útil quando alguns itens são muito mais populares.
- FIFO (First In, First Out): Remove itens mais antigos, independentemente do acesso. Simples, mas pode remover dados ainda úteis.
4.3. Cache warming
Pré-carregar dados frequentes durante a inicialização do sistema evita picos de cache miss logo após reinicializações.
text
# Cache warming durante startup
function warmupCache():
usuarios_populares = banco.buscarUsuariosMaisAcessados(1000)
for usuario in usuarios_populares:
cache.set("usuario:" + usuario.id, usuario, ttl=3600)
5. Invalidação de Cache: O Problema Mais Difícil
5.1. Invalidação explícita
Quando um dado é atualizado, a aplicação remove ou atualiza explicitamente o cache. É a abordagem mais simples, mas requer que toda modificação passe pelo mesmo ponto.
text
function atualizarUsuario(id, novos_dados):
banco.atualizarUsuario(id, novos_dados)
cache.delete("usuario:" + id) # invalidação explícita
5.2. Cache tagging
Tags permitem invalidar grupos de itens relacionados. Por exemplo, ao atualizar um artigo, invalida-se todas as entradas de cache com a tag "artigos_recentes".
text
# Redis com tags
redis.set("artigo:456", dados_artigo)
redis.sadd("tag:artigos_recentes", "artigo:456")
# Invalidação por tag
function invalidarPorTag(tag):
chaves = redis.smembers("tag:" + tag)
for chave in chaves:
redis.delete(chave)
5.3. Padrões de consistência
- Consistência eventual: Dados obsoletos são tolerados por um período. Adequado para feeds de notícias ou recomendações.
- Consistência forte: Cache sempre reflete o estado mais recente. Necessário para dados financeiros ou de inventário.
6. Armadilhas e Anti-Padrões em Cache
6.1. Cache stampede (thundering herd)
Quando muitos requests simultâneos causam cache miss e todos tentam reconstruir o cache ao mesmo tempo, sobrecarregando o banco. Soluções incluem mutex locks e "probabilistic early expiration".
text
function buscarDadosCaros(chave):
dados = cache.get(chave)
if dados is None:
lock = adquirirLock(chave)
if lock:
dados = banco.buscarDadosCaros()
cache.set(chave, dados, ttl=300)
liberarLock(chave)
else:
sleep(0.1)
return buscarDadosCaros(chave)
return dados
6.2. Cache poisoning
Inserir dados maliciosos no cache pode comprometer todo o sistema. Sempre valide dados antes de armazená-los no cache e use serialização segura.
6.3. Superdimensionamento vs. subdimensionamento
Caches muito grandes desperdiçam memória e podem aumentar custos desnecessariamente. Caches pequenos demais reduzem o hit rate. Monitore métricas como hit ratio e latência média para ajustar o tamanho ideal.
7. Monitoramento e Evolução da Estratégia de Cache
7.1. Métricas-chave
- Hit rate: Percentual de requisições atendidas pelo cache (ideal > 90%)
- Miss rate: Percentual de requisições que precisaram consultar a fonte original
- Latência de cache: Tempo médio para servir um cache hit
- Taxa de invalidação: Quantidade de entradas removidas por segundo
7.2. Testes de carga
Simule padrões de acesso reais para validar a estratégia de cache. Ferramentas como k6 ou Locust podem gerar tráfego controlado para medir hit rate e degradação sob carga.
7.3. Quando remover ou adicionar uma camada de cache
Adicione cache quando:
- A latência média ultrapassa o limite aceitável
- O banco de dados está sob carga excessiva
- Padrões de acesso repetitivos são identificados
Remova cache quando:
- O custo de manutenção supera os benefícios
- A consistência dos dados é crítica e a invalidação se torna complexa demais
- O hit rate é consistentemente baixo (< 50%)
Referências
- Redis Documentation - Caching Patterns — Guia oficial sobre padrões de cache com Redis, incluindo Cache-Aside, Read-Through e Write-Through.
- AWS - Caching Strategies and Best Practices — Visão geral da AWS sobre estratégias de cache em arquiteturas cloud, com exemplos práticos.
- Martin Fowler - Cache-Aside Pattern — Artigo clássico de Martin Fowler explicando o padrão Cache-Aside e suas variações.
- Cloudflare - What is Caching? — Explicação detalhada sobre cache em CDNs, headers HTTP e estratégias de borda.
- High Scalability - Caching Strategies and Trade-offs — Análise aprofundada sobre trade-offs entre diferentes estratégias de cache em sistemas de alta escala.
- Memcached Wiki - Eviction Algorithms — Documentação oficial do Memcached sobre algoritmos de evicção LRU e suas configurações.
- Google SRE - Cache Invalidation — Capítulo do livro Site Reliability Engineering sobre como lidar com invalidação de cache e cache stampede.