Pular para o conteúdo
10 min de leitura

Cache: quando usar, onde colocar e como não se enganar com dado velho

Por Equipe Tech do Sonne ·

Cache é a otimização mais poderosa e mais traiçoeira do desenvolvimento. Um guia sobre camadas, estratégias e o problema de verdade: a invalidação.

Neste artigo

Existe uma frase que circula entre desenvolvedores há décadas, atribuída a Phil Karlton: só há dois problemas difíceis em computação, invalidar cache e nomear coisas. A piada sobrevive porque é verdadeira. Cache é a otimização mais sedutora que existe — a promessa de deixar tudo mais rápido guardando uma cópia do resultado — e, ao mesmo tempo, a fonte de alguns dos bugs mais escorregadios de uma carreira. O usuário jura que atualizou o perfil, mas a página teima em mostrar o nome antigo. O preço mudou no banco, mas metade dos clientes ainda vê o valor de ontem. O culpado quase sempre é uma camada de cache que ninguém lembrava que estava lá.

O objetivo deste artigo não é vender cache nem assustar você para longe dele. É explicar o que ele realmente é — uma troca deliberada de consistência e memória por velocidade — e dar as ferramentas para usá-lo sabendo onde estão as armadilhas. Vamos passar pelas camadas onde o cache mora, pelas estratégias de leitura e escrita, e, principalmente, pela parte difícil: como decidir quando um dado guardado deixou de valer. Cache mal-usado não é um problema de performance; é um problema de correção, e correção é bem mais caro de consertar.

Cache é uma troca, não um brinde#

Antes de qualquer detalhe técnico, vale gravar o princípio: cache não é grátis. Você troca alguma coisa por velocidade, e é essencial saber o que está entregando na negociação.

Você troca consistência por tempo de resposta. No instante em que você guarda uma cópia de um dado, passa a existir a possibilidade de essa cópia divergir da fonte da verdade. O dado original muda, a cópia não sabe, e por algum intervalo o sistema mostra informação velha. Às vezes isso é perfeitamente aceitável — a contagem de curtidas de um post pode ficar alguns segundos atrasada sem que ninguém sofra. Às vezes é inaceitável — o saldo de uma conta bancária não pode. A primeira pergunta de todo cache é: quanta desatualização esse dado tolera?

Você troca memória por processamento. Guardar respostas ocupa espaço — RAM, disco, um servidor dedicado. Esse custo é real e cresce com o volume de coisas cacheadas. Em troca, você economiza o trabalho de recalcular ou rebuscar. Cache compensa quando o dado é caro de produzir e barato de guardar, e é lido muito mais do que muda. Se você cacheia algo que quase nunca é lido de novo, gastou memória sem ganhar nada.

O corolário é que cache não deve ser reflexo. Antes de adicionar uma camada, meça: essa operação é realmente o gargalo? É lida com frequência? Tolera atraso? Muita gente cacheia por hábito e paga com bugs de consistência que teriam sido evitados simplesmente não cacheando.

As camadas onde o cache mora#

Uma requisição típica atravessa várias oportunidades de cache, do navegador do usuário até o banco de dados. Entender essa pilha ajuda a colocar cada cache no lugar onde ele rende mais.

O navegador é o cache mais próximo do usuário. Arquivos estáticos — CSS, JavaScript, imagens — podem ser guardados no próprio navegador via cabeçalhos HTTP como Cache-Control e ETag. É o cache mais barato de todos, porque a requisição nem sai da máquina do usuário. O preço é que você perde controle: uma vez que o navegador guardou algo com validade longa, forçá-lo a buscar de novo exige truques como versionar o nome do arquivo.

``http Cache-Control: public, max-age=31536000, immutable ``

A CDN cacheia perto geograficamente. Uma rede de distribuição de conteúdo guarda cópias em servidores espalhados pelo mundo, entregando ao usuário a partir do ponto mais próximo. É excelente para conteúdo estático e para páginas que mudam pouco. CDNs modernas também cacheiam respostas dinâmicas por segundos, absorvendo picos de tráfego antes que eles cheguem à sua infraestrutura.

O cache de aplicação vive na memória do seu processo. Guardar um resultado numa variável, num mapa em memória, é o cache mais rápido do lado do servidor — nanossegundos de acesso. A limitação é que ele não é compartilhado: cada instância da aplicação tem o seu, e o que uma cacheia a outra não vê. Reiniciar o processo apaga tudo. Serve bem para dados pequenos, muito lidos e globais, como configurações.

O cache distribuído é compartilhado entre instâncias. Um Redis ou Memcached é um servidor de cache separado que todas as instâncias da aplicação consultam. Mais lento que a memória local, por causa da ida à rede, mas compartilhado e sobrevive a reinícios da aplicação. É a camada de cache "de trabalho pesado" da maioria dos sistemas: sessões, resultados de queries caras, contadores.

O banco de dados também cacheia por baixo. Bancos mantêm em memória as páginas mais acessadas e planos de execução já compilados. Você não controla isso diretamente, mas vale saber que existe: às vezes a query que parece rápida só está rápida porque os dados estão quentes na memória do banco, e a mesma query num dado frio conta outra história.

Estratégias de escrita e leitura#

Ter um cache é metade da decisão; a outra metade é definir como ele conversa com a fonte da verdade nas leituras e nas escritas. Há três padrões que cobrem quase tudo.

Cache-aside é o mais comum. A aplicação consulta o cache primeiro; se o dado está lá (hit), usa e pronto. Se não está (miss), busca na fonte, guarda no cache para a próxima vez, e devolve. O controle é todo da aplicação, o que dá flexibilidade, mas exige disciplina: é você quem precisa lembrar de invalidar quando o dado muda.

``python def get_usuario(id): u = cache.get(f"user:{id}") if u is not None: # hit return u u = banco.buscar_usuario(id) # miss: vai à fonte cache.set(f"user:{id}", u, ttl=300) return u ``

Write-through escreve nos dois ao mesmo tempo. Toda gravação vai para o cache e para a fonte de forma síncrona. A vantagem é que o cache nunca fica desatualizado em relação à última escrita; a desvantagem é que cada escrita paga o custo de atualizar as duas camadas, e você acaba cacheando dados que talvez nunca sejam lidos.

Write-back adia a escrita na fonte. A gravação vai primeiro para o cache e só depois, de forma assíncrona, é persistida na fonte. É o mais rápido para quem escreve, mas o mais arriscado: se o cache cair antes de descarregar, você perde dados. Usado onde a velocidade de escrita é crítica e uma perda pequena é tolerável, raramente para dados que não podem sumir.

A parte difícil: invalidação#

Guardar dado é fácil. Saber a hora exata de jogá-lo fora é onde mora a dor. Um cache que nunca invalida serve lixo; um que invalida demais não cacheia nada. Há duas grandes abordagens, e a maioria dos sistemas mistura as duas.

TTL: deixe o dado expirar sozinho. Você define um tempo de vida — 30 segundos, 5 minutos, uma hora — e o cache descarta a entrada automaticamente quando ele passa. É simples, robusto e não exige que nada avise o cache sobre mudanças. O preço é aceitar uma janela de desatualização do tamanho do TTL: durante esse intervalo, o dado pode estar velho e ninguém percebe. TTL é a escolha certa quando um atraso limitado é tolerável e você prefere simplicidade a precisão.

Invalidação por evento: apague quando o dado muda. Aqui, quando a fonte muda, algo dispara a remoção (ou atualização) da entrada correspondente no cache. É preciso e mantém tudo fresco, mas é frágil: você precisa lembrar de invalidar em todos os caminhos que alteram aquele dado, e esquecer um deles produz exatamente o bug de "o valor mudou mas o cache insiste no antigo". Quanto mais lugares escrevem no dado, mais difícil garantir que todos avisam o cache.

A escolha honesta costuma ser combinar: invalidação por evento para manter o cache correto no caso comum, com um TTL de segurança por baixo, para que qualquer invalidação esquecida se corrija sozinha depois de um tempo, em vez de servir dado velho para sempre.

Chaves de cache que não se atropelam#

Um cache é, no fundo, um dicionário gigante, e a qualidade das chaves decide se ele funciona ou vira uma fonte de bugs sutis.

A chave precisa capturar tudo que muda a resposta. Se a resposta depende do usuário, do idioma e da moeda, os três precisam entrar na chave. Uma chave que ignora uma dessas dimensões faz um usuário receber o dado cacheado de outro — o tipo de vazamento silencioso que passa despercebido em testes e vira incidente de privacidade em produção. Uma chave boa é específica o bastante para não colidir e estável o bastante para dar hit.

Cuidado com a granularidade. Chaves largas demais — cachear uma página inteira que contém um pedaço personalizado — forçam invalidações constantes ou servem conteúdo errado. Chaves estreitas demais — uma entrada para cada combinação rara de filtros — geram um cache cheio de itens que nunca são reusados, desperdiçando memória sem melhorar a taxa de acerto. Encontrar o nível certo é parte do projeto, não um detalhe.

Stampede: o rebanho que derruba o banco#

Existe um modo de falha específico de cache que merece atenção porque ataca justamente na hora de pico. Chama-se cache stampede ou thundering herd.

Muitos pedidos ao mesmo dado expirado, ao mesmo tempo. Imagine uma entrada muito popular que acabou de expirar. No instante seguinte, mil requisições dão miss simultaneamente, todas concluem que precisam recalcular, e todas batem na fonte ao mesmo tempo. O banco, que estava tranquilo porque o cache o protegia, recebe de repente mil queries idênticas e caras. A camada que existia para aliviar a carga vira o gatilho de uma sobrecarga.

As defesas atacam a simultaneidade. Um lock que permite que apenas um pedido recalcule enquanto os outros esperam ou usam o valor antigo; recomputação antecipada, que renova a entrada um pouco antes de ela expirar de verdade; ou espalhar os TTLs com um pouco de aleatoriedade, para que não expirem todos no mesmo segundo. O ponto comum é impedir que a expiração de um item quente vire uma avalanche coordenada contra a fonte.

Quando NÃO cachear#

Terminar reforçando os limites é mais útil do que empilhar mais técnicas, porque o erro mais caro com cache é usá-lo onde não deveria.

Não cacheie dado que precisa ser sempre exato. Saldo financeiro, estoque no momento da compra, permissões de acesso — dados em que servir uma versão de alguns segundos atrás causa dano real. Aqui a consistência vale mais que a latência, e o atalho custa caro.

Não cacheie o que quase nunca é relido. Se cada entrada é consultada uma ou nenhuma vez antes de mudar, você paga memória e complexidade de invalidação sem colher o benefício. Cache rende quando a razão entre leituras e escritas é alta; quando ela é baixa, ele só adiciona um lugar a mais para o bug se esconder.

Não adicione cache antes de medir. A resposta certa para "está lento" é primeiro descobrir o que está lento. Muitas vezes o gargalo é uma query sem índice ou um laço N+1, e resolver a causa elimina a necessidade do cache — que teria mascarado o problema real enquanto cobrava juros em bugs de consistência. Cache é uma ferramenta poderosa; como toda ferramenta poderosa, o custo de usá-la no lugar errado é proporcional ao poder que ela tem.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly