Cache é, provavelmente, a técnica com melhor relação custo-benefício para melhorar performance de uma aplicação web. Diferente de otimizações que exigem reescrever código ou trocar infraestrutura, uma estratégia de cache bem aplicada pode reduzir tempos de resposta de segundos para milissegundos com relativamente pouco esforço de implementação. O problema é que cache mal aplicado também é uma das fontes mais comuns de bugs sutis — dados desatualizados sendo exibidos ao usuário. Neste artigo, explicamos as principais estratégias e como evitar as armadilhas mais comuns.
Cache-aside: a estratégia mais comum
No padrão cache-aside (também chamado lazy loading), a aplicação primeiro verifica se o dado está no cache. Se estiver, retorna direto. Se não estiver, busca no banco de dados, armazena no cache para futuras requisições e então retorna o resultado. Essa abordagem é simples de implementar e garante que apenas dados efetivamente solicitados ocupem espaço no cache, mas tem a desvantagem de que a primeira requisição após uma expiração de cache sempre será mais lenta, já que precisa consultar o banco.
def buscar_produto(produto_id):
cache_key = f"produto:{produto_id}"
produto = cache.get(cache_key)
if produto is None:
produto = db.query(Produto).get(produto_id)
cache.set(cache_key, produto, timeout=3600)
return produto
Write-through: mantendo cache e banco sempre sincronizados
Na estratégia write-through, toda escrita é feita simultaneamente no banco de dados e no cache, garantindo que o cache nunca fique desatualizado em relação ao banco. O custo é uma escrita ligeiramente mais lenta (já que duas operações acontecem a cada atualização), mas o ganho em consistência costuma valer a pena para dados críticos que não podem ficar defasados, como saldo de conta ou status de pedido.
Cache de consultas versus cache de objetos
Vale diferenciar dois níveis de cache: cache de objetos individuais (como o exemplo acima, armazenando um produto específico) e cache de resultados de consultas complexas (como uma listagem filtrada e ordenada). O segundo tipo é mais delicado, já que qualquer mudança em qualquer um dos registros envolvidos pode invalidar o resultado cacheado — por isso, esse tipo de cache costuma ter TTLs mais curtos ou estratégias de invalidação mais agressivas.
Cache não é sobre guardar dados por mais tempo possível — é sobre guardar exatamente pelo tempo em que a informação ainda é confiável.
Invalidação de cache: o problema mais difícil da computação
Existe uma piada clássica entre engenheiros de que só existem dois problemas difíceis em ciência da computação: nomear variáveis e invalidar cache. A dificuldade está em garantir que, quando um dado muda, todas as entradas de cache relacionadas a ele sejam atualizadas ou removidas — especialmente quando o mesmo dado aparece em múltiplas chaves de cache diferentes (o produto individual, a listagem da categoria, os resultados de busca). Estratégias comuns incluem invalidação explícita no momento da escrita, uso de tags para agrupar chaves relacionadas, ou simplesmente TTLs curtos o suficiente para que a inconsistência temporária seja aceitável.
Escolhendo o TTL correto para cada tipo de dado
Nem todo dado deve ter o mesmo tempo de expiração. Configurações que raramente mudam podem ter TTL de horas ou até dias. Listagens de produtos podem ter TTL de minutos. Dados sensíveis a mudanças em tempo real, como saldo ou disponibilidade de estoque, exigem TTLs muito curtos ou uma estratégia de invalidação ativa em vez de expiração passiva. Definir isso caso a caso, em vez de aplicar um TTL genérico para toda a aplicação, é o que separa uma estratégia de cache eficaz de uma que só transfere o problema de performance para um problema de consistência.
Cache em múltiplas camadas
Aplicações de grande escala costumam usar cache em várias camadas simultaneamente: cache no navegador do usuário (via headers HTTP), CDN para conteúdo estático, cache de aplicação (Redis/Memcached) para dados de negócio, e até cache interno do banco de dados. Cada camada tem seu próprio propósito e tempo de vida, e entender essa hierarquia ajuda a decidir onde investir esforço de otimização primeiro — geralmente, quanto mais próxima do usuário a camada de cache, maior o ganho de performance percebido.
Monitorando a eficácia do cache
Implementar cache sem medir sua taxa de acerto (hit rate) é decidir às cegas. Uma taxa de acerto baixa pode indicar que o TTL está curto demais, que as chaves de cache estão mal definidas, ou que o padrão de acesso aos dados é mais aleatório do que se imaginava — nesses casos, o cache pode estar consumindo memória sem trazer o benefício esperado. Monitorar essa métrica continuamente permite ajustar a estratégia com dados reais em vez de suposições.
Considerações finais
Cache bem implementado é uma das formas mais eficientes de melhorar performance sem reescrever a aplicação, mas exige decisões cuidadosas sobre o que cachear, por quanto tempo e como invalidar corretamente. Aplicar cache de forma genérica, sem considerar o padrão de acesso e a criticidade de cada dado, tende a trocar um problema de performance por um problema de consistência. Se sua aplicação está enfrentando lentidão e você quer avaliar onde o cache pode ajudar, a equipe da ASL Software Engineering pode fazer essa análise. Fale com a gente.