Cache: a solução que vira dor de cabeça quando você menos espera

Tem vídeo desse conteúdo no canal Veja o passo a passo completo em vídeo.
Assistir no YouTube →
Cache: a solução que vira dor de cabeça quando você menos espera

Existe uma piada antiga na computação que diz que só há dois problemas difíceis de verdade: invalidar cache, nomear coisas e erros de contagem. A piada é boba, mas o recado sobre cache é sério. Adicionar cache é a coisa mais fácil do mundo. Manter ele coerente com a realidade é onde mora a dor.

O cache resolve um problema real: buscar o mesmo dado caro repetidamente é desperdício. Mas, no instante em que você guarda uma cópia, cria um segundo problema — e agora existem duas versões da verdade que podem discordar.

O trade-off que ninguém escapa

Cache é, no fundo, uma aposta: “esse dado não vai mudar tão rápido, então vale guardar uma cópia por perto”. Quando a aposta acerta, você economiza uma consulta cara e a resposta voa. Quando erra, você serve dado velho — e, dependendo do que for, isso vai de irrelevante a desastroso.

Um preço desatualizado por três segundos numa página de blog: ninguém morre. Um saldo bancário desatualizado, ou um estoque que diz “disponível” quando já esgotou: aí a conta chega. Por isso, a primeira pergunta antes de cachear qualquer coisa é: qual o custo de esse dado estar errado por um tempo?

As estratégias, e o que cada uma cobra

Existem algumas formas clássicas de manter o cache honesto. Nenhuma é a “certa” — cada uma troca uma dor por outra.

Expiração por tempo (TTL)

A mais simples: cada entrada vive por X segundos e depois morre sozinha. É fácil de implementar e previsível. O custo é que você aceita, de propósito, servir dado potencialmente velho durante a janela do TTL. Para muita coisa, é o suficiente. Um TTL de 30 segundos numa listagem de produtos raramente vai incomodar alguém.

Invalidar na escrita

Aqui você é ativo: quando o dado muda, você apaga (ou atualiza) a entrada do cache na mesma hora. Fica bem mais fresco. O preço é o acoplamento — todo lugar que escreve precisa lembrar de mexer no cache. Esquecer um único caminho de escrita é o suficiente para deixar uma cópia velha viva para sempre, e esse tipo de bug é traiçoeiro de rastrear.

func atualizarPreco(id, valor) {
  db.update(id, valor)
  cache.delete("produto:" + id)  // fácil de esquecer
}

A chave que muda com o dado

Um truque elegante é embutir uma “versão” na chave do cache — a data de atualização do registro, por exemplo. Quando o dado muda, a chave muda junto, e a entrada antiga simplesmente deixa de ser consultada (e some sozinha depois). Você troca o problema de invalidar pelo problema de gerar chaves consistentes, o que costuma ser mais fácil de acertar.

As armadilhas que aparecem no pior momento

Além da coerência, cache tem dois problemas operacionais que só se manifestam sob carga — ou seja, exatamente quando você menos quer.

  • Estouro em massa (cache stampede). Uma entrada popular expira, e mil requisições que dependiam dela batem no banco ao mesmo tempo para recalcular. O cache que existia para proteger o banco vira o gatilho de uma avalanche. Mitiga-se deixando só a primeira requisição recalcular enquanto as outras esperam.
  • Chave inexistente batendo sempre no banco. Se você cacheia resultados mas não cacheia “esse dado não existe”, buscas por IDs inválidos furam o cache toda vez. Vale guardar o “vazio” também, com um TTL curto.

Cache não é uma feature que você liga e esquece. É um sistema paralelo de verdade que precisa combinar com o original — e que vai divergir no dia em que você deixar de prestar atenção.

Cache não vive num lugar só

Um detalhe que confunde bastante gente: existe cache em várias camadas ao mesmo tempo, e elas nem sempre concordam entre si. O navegador guarda uma cópia. Uma CDN na frente guarda outra. A sua aplicação guarda a dela. O banco tem o próprio cache interno. Quando um dado muda, você pode ter limpado o cache da aplicação e ainda estar servindo a versão velha que a CDN memorizou.

Isso explica aquele bug clássico: “atualizei, mas continua aparecendo o antigo — e só para alguns usuários”. Cada camada tem seu próprio ciclo de vida, e o dado velho pode estar preso em qualquer uma delas. Ao desenhar cache, vale mapear quantas cópias da verdade existem no caminho entre o banco e o olho do usuário.

Quanto mais camadas, mais rápido o sistema — e mais lugares para o dado velho se esconder. É o mesmo trade-off de sempre, agora multiplicado.

A regra que economiza sofrimento

Antes de adicionar cache, meça. Muita gente coloca cache por reflexo, para “melhorar performance”, sem nunca ter confirmado que aquele ponto é lento de verdade. Você adiciona uma camada inteira de complexidade — e de bugs de dado velho — para resolver um problema que talvez nem existisse.

Quando o cache for realmente necessário, comece pelo mais simples que resolve: um TTL curto costuma entregar 90% do ganho com 10% da dor. Invalidação ativa e esquemas mais espertos entram quando o dado velho começa a custar caro de verdade. Cache é ótimo servo e péssimo patrão — trate ele com o respeito de quem sabe que, um dia, ele vai te servir a versão errada da verdade.

Leia também

Prefere ver em vídeo?

Esse artigo virou um vídeo completo no canal, com diagramas animados e demo ao vivo.

Assistir no YouTube →