"Mas eu já corrigi isso"
O cliente avisa que o preço do produto está errado no site. Você abre o banco: 89,90. Correto.
Abre a página: 129,90.
Você acha que enlouqueceu. Dá um dd() no controller — vem 89,90. Tira o dd(), recarrega: 129,90 de novo. Você começa a duvidar do navegador, do Nginx, da existência.
Aí você lembra daquele Cache::remember() que você mesmo escreveu três meses atrás. Com TTL de 24 horas.
O bug não existe. O cache está te mostrando um retrato de ontem — e defendendo esse retrato com unhas e dentes.
O remember() que todo mundo escreve
public function destaques()
{
// resolvido! a home ficou 400ms mais rápida 🎉
return Cache::remember('produtos.destaques', 3600 * 24, function () {
return Produto::destaque()->with('categoria')->get();
});
}
Essa linha é linda. Ela resolve o problema de performance de verdade, na hora, e o gráfico do APM agradece.
Só que ela responde metade da pergunta. Você definiu como guardar. Não definiu quando esquecer.
E o Produto::update() que roda no painel do admin não tem a menor ideia de que essa chave existe. Ninguém apresentou os dois.
Cache é cópia. E cópia envelhece.
Antes da solução, o jeito certo de pensar: cache não é uma otimização mágica, é uma segunda fonte de verdade que você acabou de criar no seu sistema.
Toda vez que você guarda algo em cache, você assume um compromisso: manter duas coisas sincronizadas. O banco e a cópia.
Só existem três jeitos de honrar esse compromisso. Todo cache decente usa um deles.
Estratégia 1: quem escreve, esquece
A mais direta. Quando o dado muda, mata a chave. Um Observer resolve isso sem espalhar Cache::forget() pelo projeto inteiro:
class ProdutoObserver
{
public function saved(Produto $produto): void
{
Cache::forget('produtos.destaques');
Cache::forget("produto:{$produto->id}");
}
public function deleted(Produto $produto): void
{
$this->saved($produto);
}
}
Funciona bem e é fácil de ler. O ponto fraco aparece com o tempo: cada chave nova que você criar precisa entrar nessa lista. Esquecer uma é o bug do começo do post voltando.
Estratégia 2: chave que se aposenta sozinha
Aqui vem o truque que eu mais gosto, porque não precisa invalidar nada: coloque a versão do dado dentro da chave.
public function fichaTecnica(Produto $produto): array
{
// updated_at muda no save → a chave muda → o valor velho vira órfão
$chave = "produto:{$produto->id}:ficha:{$produto->updated_at->timestamp}";
return Cache::remember($chave, 3600 * 24, function () use ($produto) {
return $produto->montarFichaTecnica(); // aquele cálculo caro
});
}
Salvou o produto? O updated_at mudou, a chave mudou, o valor antigo simplesmente nunca mais é pedido e expira sozinho.
Não tem forget(), não tem Observer, não tem lista pra manter. E funciona no driver file, no database, em qualquer um.
O custo é lixo acumulado no cache até o TTL vencer. Em troca você ganha algo raro: é impossível esquecer de invalidar.
Estratégia 3: tags, quando é um grupo inteiro
Quando um único evento invalida vários caches de uma vez, tag é o caminho:
Cache::tags(['produtos', "categoria:{$produto->categoria_id}"])
->remember($chave, 3600, fn () => $produto->relacionados());
// depois, quando a categoria muda de nome:
Cache::tags(["categoria:{$id}"])->flush();
Detalhe importante: tags só funcionam em Redis e Memcached. Se seu CACHE_STORE é file ou database, isso estoura na sua cara. É provavelmente o motivo número um de "funciona local, quebra em produção" (ou o contrário) quando o assunto é cache.
A pegadinha: o cache que mente no teste
Essa merece atenção porque é traiçoeira.
No phpunit.xml, o CACHE_STORE normalmente é array — um cache que morre no fim de cada teste. Ótimo pra isolamento, péssimo pra confiança: o teste passa mesmo quando sua invalidação está errada, porque o cache nunca sobrevive o suficiente pra servir dado velho.
Se o cache faz parte da regra de negócio, teste a invalidação de propósito:
it('esquece o cache dos destaques quando um produto muda', function () {
Cache::put('produtos.destaques', collect(['velho']), 3600);
Produto::factory()->create()->update(['nome' => 'novo']);
expect(Cache::has('produtos.destaques'))->toBeFalse();
});
E o parente confuso: php artisan config:cache não tem relação nenhuma com Cache:: — é outro assunto, outro arquivo, outra dor (essa eu já contei em outro post 😄).
Bônus: o dado velho que serve pra caramba
Às vezes o problema não é dado desatualizado, é o oposto: a chave expira, chegam 300 requests ao mesmo tempo e todos vão bater no banco pra recalcular a mesma coisa. É o famoso cache stampede.
O Laravel tem uma resposta pronta:
// fresquinho por 5 min; até 30 min serve o valor velho e recalcula em background
Cache::flexible('relatorio.vendas', [300, 1800], fn () => $this->calcular());
Um request só paga o preço do recálculo. Os outros recebem o valor um pouco velho na hora, em vez de ficarem na fila do banco. Pra dashboard e relatório, isso é troca boa demais pra ignorar.
Antes de você fechar a aba
Antes de escrever Cache::remember(), responda uma pergunta: quem apaga isso, e quando?
Se você não sabe, não é cache — é um bug com data marcada. Pode ser Observer, pode ser chave versionada, pode ser TTL curto porque o dado suporta ficar dois minutos velho. Qualquer resposta serve. A ausência de resposta é que custa caro.
E me conta uma coisa: quanto tempo você já perdeu caçando um bug que era só cache? Eu já passei uma tarde inteira num "erro de cálculo" que era um Cache::remember de 24h que eu mesmo tinha escrito na semana anterior. 🙃
Top comments (0)