SYSTEM DESIGN · CACHE · LEITURA DE ~12 MIN
Estratégias de Cache
Cache não é apenas “colocar Redis na frente do banco”. A forma como a aplicação lê, escreve, atualiza e invalida os dados em cache muda completamente o comportamento do sistema.
SEÇÃO 01
O que é cache?
Cache é uma camada de armazenamento rápida usada para manter temporariamente dados que seriam mais caros ou demorados para buscar novamente.
Usuário
↓
Aplicação
↓
Banco de dados
Usuário
↓
Aplicação
↓
Cache
├── dado encontrado → retorna rapidamente
└── dado ausente → consulta o banco
Imagine que 10 mil pessoas consultem o mesmo produto. Sem cache, são 10 mil requisições e 10 mil consultas ao banco. Com cache, a primeira consulta busca o dado no banco, salva no cache e as próximas requisições são atendidas pela camada rápida.
O cache funciona como uma camada de proteção, reduzindo leituras repetidas, latência e pressão sobre o banco de dados. Entretanto, surge um novo desafio: como manter o cache sincronizado com a fonte original dos dados?
SEÇÃO 02
1. Cache-aside
Cache-aside é uma das estratégias mais comuns. Nela, a aplicação é responsável por consultar tanto o cache quanto o banco.
Aplicação
↓
Cache
├── Cache hit → retorna
└── Cache miss
↓
Banco
↓
Salva no cache
↓
Retorna
O fluxo é direto: a aplicação procura o dado no cache. Se encontrar, retorna. Se não encontrar, consulta o banco, salva o resultado no cache e retorna o dado.
def get_user(user_id):
user = cache.get(f"user:{user_id}")
if user is None:
user = database.get_user(user_id)
if user is not None:
cache.set(f"user:{user_id}", user, ttl=600)
return user
A primeira requisição pode ser mais lenta porque precisa consultar o banco. As próximas são atendidas pelo cache. Essa estratégia também é conhecida como lazy loading, pois somente os dados realmente solicitados são armazenados.
Atualizações no cache-aside
Quando um dado é alterado, normalmente a aplicação atualiza o banco e invalida o cache. A próxima leitura busca o valor atualizado.
def update_user(user_id, values):
database.update_user(user_id, values)
cache.delete(f"user:{user_id}")
- Vantagens: implementação relativamente simples, uso de memória apenas para dados acessados e grande redução de leituras no banco.
- Desvantagens: a aplicação precisa gerenciar cache e banco, a primeira leitura sofre cache miss, e dados podem ficar desatualizados se a invalidação falhar.
SEÇÃO 03
2. Read-through
Read-through é parecido com cache-aside, mas existe uma diferença importante: a aplicação consulta apenas o cache. Quando o dado não existe, o próprio cache busca no banco.
Aplicação
↓
Cache
├── encontrou → retorna
└── não encontrou
↓
Cache consulta o banco
↓
Cache armazena o dado
↓
retorna
A aplicação executa algo parecido com cache.get("user:123"). Internamente, a infraestrutura de cache é responsável por carregar o dado.
No cache-aside, a aplicação consulta o banco em um cache miss. No read-through, o próprio cache consulta o banco.
- Vantagens: simplifica o código da aplicação, centraliza a lógica de carregamento e padroniza o comportamento de cache.
- Desvantagens: exige uma camada de cache mais inteligente, aumenta o acoplamento com a solução de cache e nem toda ferramenta oferece suporte nativo.
SEÇÃO 04
3. Write-through
No write-through, toda escrita passa pelo cache. O cache salva o dado e grava de forma síncrona no banco antes de confirmar a operação.
Aplicação
↓
Cache
↓
Banco de dados
↓
Confirmação
Quando o usuário atualiza seu nome, a aplicação envia a atualização ao cache, o cache grava a alteração no banco, mantém o novo valor e só então a aplicação retorna sucesso.
Como o banco precisa ser atualizado antes da resposta, a escrita pode ser mais lenta. Entretanto, quando a operação termina, o cache já possui o valor atualizado.
- Vantagens: cache e banco ficam sincronizados, há menor risco de retornar dados desatualizados e leituras realizadas logo após a escrita são rápidas.
- Desvantagens: a escrita continua dependendo da velocidade do banco, cada escrita no cache gera uma escrita no banco e dados que nunca serão lidos também podem ocupar cache.
Write-through não existe para reduzir a quantidade de escritas no banco. Seu principal objetivo é garantir consistência entre cache e banco.
SEÇÃO 05
4. Write-behind
Write-behind, também chamado de write-back, prioriza velocidade de escrita. A aplicação grava no cache e recebe confirmação imediatamente. A persistência no banco acontece depois, de forma assíncrona.
Aplicação
↓
Cache
↓
Resposta ao usuário
Enquanto isso:
Cache ou fila
↓
Worker
↓
Banco de dados
Essa estratégia é útil quando o sistema precisa responder rapidamente e pode aceitar consistência eventual. Ela também pode amortecer picos de escrita: 10 mil requisições entram no cache e em uma fila, enquanto o banco processa no ritmo suportado.
Também é possível consolidar várias alterações. Um contador de 1.000 visualizações pode ser atualizado no cache muitas vezes e persistido no banco como uma atualização consolidada.
- Vantagens: resposta rápida, redução de picos de escrita, processamento em lote, maior throughput e desacoplamento entre aplicação e banco.
- Desvantagens: risco de perda de dados, diferença temporária entre banco e cache, maior complexidade operacional, retries, monitoramento e controle de backlog.
Cache atualizado, banco ainda não atualizado, cache falha: o dado pode ser perdido. Por isso, implementações robustas usam filas duráveis, persistência, idempotência e reprocessamento.
SEÇÃO 06
5. Refresh-ahead
Refresh-ahead atualiza os dados antes que o TTL expire. A ideia é manter dados populares sempre quentes no cache.
10:00 → produto salvo no cache
10:10 → cache expira
10:11 → próximo usuário consulta
10:11 → aplicação busca no banco
10:00 → produto salvo no cache
10:08 → sistema percebe que ele continua popular
10:09 → atualiza antecipadamente
10:10 → item continua disponível
Enquanto o dado continuar sendo muito acessado, ele pode permanecer quente no cache. Isso reduz cache misses e evita que o usuário espere depois de uma expiração.
- Vantagens: reduz cache misses, mantém dados populares disponíveis e melhora a latência de leituras previsíveis.
- Desvantagens: pode atualizar dados que não serão usados novamente, aumentar consultas ao banco e depender de uma boa previsão de popularidade.
SEÇÃO 07
Comparação entre as estratégias
Colocando as estratégias lado a lado, fica mais claro o que cada uma tenta otimizar.
SEÇÃO 08
Qual estratégia escolher?
Não existe uma estratégia universalmente melhor. Cache-aside costuma funcionar bem como ponto inicial para aplicações com muitas leituras. Write-through é indicado quando a aplicação precisa garantir que cache e banco estejam sincronizados antes de confirmar a escrita.
Write-behind faz sentido quando baixa latência e alto volume de escrita são mais importantes, desde que o sistema consiga lidar com consistência eventual. Refresh-ahead funciona melhor para dados populares e previsíveis. Read-through é interessante quando a infraestrutura pode esconder da aplicação toda a lógica de carregamento.
Também é possível combinar estratégias: cache-aside para leituras, invalidação após atualização e refresh-ahead para itens populares. Ou write-through para dados críticos e write-behind para métricas e contadores.
Qual é o volume de leituras? Qual é o volume de escritas? O sistema aceita consistência eventual? Qual é o impacto de perder uma atualização? Quanto tempo o dado pode permanecer desatualizado?
Cache não é apenas uma ferramenta para armazenar dados temporários. Ele faz parte da arquitetura e influencia diretamente consistência, latência, disponibilidade e pressão sobre o banco de dados.
A melhor estratégia não é aquela que parece mais rápida no diagrama. É aquela que oferece o equilíbrio correto entre performance, consistência e complexidade para o contexto da aplicação.
FIM · OBRIGADO POR LER
Cache bom é decisão de arquitetura.
Quando o sistema cresce, a pergunta deixa de ser “tem cache?” e passa a ser “qual contrato esse cache tem com o banco?”.