Cache do Laravel: o básico, mais dicas e truques

A técnica de armazenamento em cache permite criar aplicativos mais escalonáveis, armazenando os resultados de algumas consultas em um armazenamento rápido na memória. No entanto, o cache implementado incorretamente pode prejudicar bastante a impressão do usuário sobre o seu aplicativo. Este artigo contém alguns conceitos básicos sobre armazenamento em cache, várias regras e tabus que aprendi em vários projetos anteriores.


Não use cache.


Seu projeto é rápido e não apresenta problemas de desempenho?
Esqueça o cache. Sério :)


Isso complicará bastante as operações de leitura do banco de dados sem nenhum benefício.


É verdade que Mohamed Said, no início deste artigo, faz alguns cálculos e prova que, em alguns casos, otimizar o aplicativo por milissegundos pode economizar uma tonelada de dinheiro em sua conta da AWS. Portanto, se a economia projetada em seu projeto for superior a US $ 1,86, talvez o cache seja uma boa idéia.


Como isso funciona?


Quando um aplicativo deseja obter alguns dados do banco de dados, por exemplo, a entidade Post por seu ID, ele gera uma chave de cache exclusiva para este caso ( 'post_' . $id é bastante adequado) e tenta encontrar o valor dessa chave no armazenamento rápido de valores-chave (memcache, redis ou outro). Se o valor estiver lá, o aplicativo o usará. Caso contrário, ele será retirado do banco de dados e armazenado no cache por essa chave para uso futuro.



Manter esse valor no cache não é uma boa ideia para sempre, pois essa entidade Post pode ser atualizada, mas o aplicativo sempre receberá o valor antigo em cache.
Portanto, as funções de cache geralmente perguntam a que horas esse valor deve ser armazenado.


Após esse tempo expirar, o memcache ou o redis "esquecem" e o aplicativo recebe um novo valor do banco de dados.


Um exemplo:


 public function getPost($id): Post { $key = 'post_' . $id; $post = \Cache::get($key); if($post === null) { $post = Post::findOrFail($id); \Cache::put($key, $post, 900); } return $post; } 

Aqui, coloquei a entidade Post no cache por 15 minutos (desde a versão 5.8, o laravel usa segundos neste parâmetro, antes que houvesse minutos). A fachada do Cache também possui um método conveniente de remember para este caso. Este código faz exatamente a mesma coisa que o anterior:


 public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { return Post::findOrFail($id); }); } 

Há um capítulo sobre cache na documentação do Laravel que explica como instalar os drivers necessários para o seu aplicativo e as principais funcionalidades.


Dados em cache


Todos os drivers Laravel padrão armazenam dados como seqüências de caracteres. Quando pedimos que você armazene em cache uma instância do modelo Eloquent, ele usa a função serialize para obter a string do objeto. A função unserialize restaura o estado de um objeto quando o obtemos do cache.


Quase todos os dados podem ser armazenados em cache. Números, seqüências de caracteres, matrizes, objetos (se eles puderem ser serializados corretamente, consulte as descrições das funções pelos links anteriormente).


Entidades e coleções eloquentes podem ser facilmente armazenadas em cache e são os valores mais populares no cache do aplicativo Laravel. No entanto, o uso de outros tipos também é praticado amplamente. O método Cache::increment é popular para implementar vários contadores. Além disso, bloqueios atômicos são bastante úteis quando os desenvolvedores estão enfrentando condições de corrida .


O que armazenar em cache?


Os primeiros candidatos ao cache são solicitações executadas com muita frequência, mas seu plano de execução não é o mais fácil. O melhor exemplo são os 5 principais artigos da página principal ou as últimas notícias. Armazenar em cache esses valores pode melhorar muito o desempenho da página principal.


Geralmente, a busca de entidades pelo id usando Model::find($id) é muito rápida, mas se essa tabela estiver muito carregada com inúmeras atualizações, insira e exclua consultas, reduzir o número de consultas selecionadas dará uma boa trégua ao banco de dados. As entidades com relacionamentos hasMany que serão carregadas toda vez também são boas candidatas ao cache. Quando trabalhei em um projeto com mais de 10 milhões de visitantes por dia, armazenamos em cache quase qualquer solicitação selecionada.


Invalidação de cache


A deterioração da chave após um tempo especificado ajuda a atualizar os dados no cache, mas isso não acontece imediatamente. O usuário pode alterar os dados, mas por algum tempo ele continuará vendo a versão antiga deles no aplicativo. O diálogo habitual em um dos meus projetos anteriores:


 :   ,     ! : ,  15 ( ,  )... 

Esse comportamento é muito inconveniente para os usuários, e a decisão óbvia de excluir dados antigos do cache quando os atualizamos rapidamente vem à mente. Esse processo é chamado de deficiência. Para chaves simples como "post_%id%" , a "post_%id%" não "post_%id%" muito difícil.


Eventos eloquentes podem ajudar ou, se o seu aplicativo gerar eventos especiais como PostPublished ou UserBanned , pode ser ainda mais simples. Exemplo com eventos Eloquent. Primeiro você precisa criar classes de eventos. Por conveniência, usarei uma classe abstrata para eles:


 abstract class PostEvent { /** @var Post */ private $post; public function __construct(Post $post) { $this->post = $post; } public function getPost(): Post { return $this->post; } } final class PostSaved extends PostEvent{} final class PostDeleted extends PostEvent{} 

Obviamente, de acordo com o PSR-4, cada classe deve estar em seu próprio arquivo. Configure a classe Post Eloquent (usando a documentação ):


 class Post extends Model { protected $dispatchesEvents = [ 'saved' => PostSaved::class, 'deleted' => PostDeleted::class, ]; } 

Crie um ouvinte para estes eventos:


 class EventServiceProvider extends ServiceProvider { protected $listen = [ PostSaved::class => [ ClearPostCache::class, ], PostDeleted::class => [ ClearPostCache::class, ], ]; } class ClearPostCache { public function handle(PostEvent $event) { \Cache::forget('post_' . $event->getPost()->id); } } 

Este código removerá os valores em cache após cada atualização ou exclusão das entidades Post. A invalidação de listas de entidades, como os cinco principais artigos ou as últimas notícias, será um pouco mais complicada. Eu vi três estratégias:


Não desabilite a estratégia


Só não toque nesses valores. Normalmente, isso não traz problemas. Tudo bem que as novas notícias apareçam na lista das últimas um pouco mais tarde (é claro, se este não for um grande portal de notícias). Mas, para alguns projetos, é realmente importante ter novos dados nessas listas.


Encontrar e desativar estratégia


Cada vez que você atualiza uma publicação, você pode tentar encontrá-la nas listas em cache e, se houver, excluir esse valor em cache.


 public function getTopPosts() { return \Cache::remember('top_posts', 900, function() { return Post::/*   top-5*/()->get(); }); } class CheckAndClearTopPostsCache { public function handle(PostEvent $event) { $updatedPost = $event->getPost(); $posts = \Cache::get('top_posts', []); foreach($posts as $post) { if($updatedPost->id == $post->id) { \Cache::forget('top_posts'); return; } } } } 

Parece feio, mas funciona.


Estratégia "ID da loja"


Se a ordem dos itens da lista não for importante, apenas o ID das entradas poderá ser armazenado no cache. Depois de receber o id, você pode criar uma lista de chaves no formato 'post_'.$id e obter todos os valores usando o método Cache::many , que obtém muitos valores do cache em uma solicitação (isso também é chamado de multi get).


A invalidação do cache não é em vão denominada uma das duas dificuldades na programação e é muito difícil em alguns casos.


Cache de Relacionamento


Armazenar em cache entidades com relacionamentos requer maior atenção.


 $post = Post::findOrFail($id); foreach($post->comments...) 

Este código executa duas consultas SELECT . Obtendo entidade por id e comentários por post_id . Implementamos o cache:


 public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { return Post::findOrFail($id); }); } $post = getPost($id); foreach($post->comments...) 

O primeiro pedido foi armazenado em cache e o segundo não. Quando o driver do cache grava Post no cache, os comments ainda não são carregados. Se também queremos armazená-los em cache, devemos carregá-los manualmente:


 public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { $post = Post::findOrFail($id); $post->load('comments'); return $post; }); } 

Agora, ambas as solicitações estão armazenadas em cache, mas precisamos invalidar os valores de 'post_'.$id toda vez que um comentário é adicionado. Não é muito eficiente, portanto, é melhor armazenar o cache de comentários separadamente:


 public function getPostComments(Post $post) { return \Cache::remember('post_comments_' . $post->id, 900, function() use ($post) { return $post->comments; }); } $post = getPost($id); $comments = getPostComments($post); foreach($comments...) 

Às vezes, a essência e a atitude estão fortemente conectadas entre si e são sempre usadas juntas (ordem com detalhes, publicação com tradução para o idioma desejado). Nesse caso, armazená-los em um cache é bastante normal.


Fonte única de verdade para chaves de cache


Se o projeto implementa a invalidação, as chaves de cache são geradas em pelo menos dois locais: para chamar Cache::get / Cache::remember e para chamar Cache::forget . Já encontrei situações em que essa chave foi alterada em um lugar, mas não em outro, e a incapacidade se rompeu. O conselho usual para esses casos é constante, mas as chaves de cache são geradas dinamicamente, então eu uso classes especiais que geram chaves:


 final class CacheKeys { public static function postById($postId): string { return 'post_' . $postId; } public static function postComments($postId): string { return 'post_comments' . $postId; } } \Cache::remember(CacheKeys::postById($id), 900, function() use ($id) { $post = Post::findOrFail($id); }); // .... \Cache::forget(CacheKeys::postById($id)); 

A vida útil das chaves também pode ser renderizada em constantes para melhor legibilidade. Esses 900 ou 15 * 60 aumentam a carga cognitiva ao ler o código.


Não use cache em operações de gravação


Ao implementar operações de gravação, como alterar o título ou o texto de uma publicação, é tentador usar o método getPost escrito anteriormente:


 $post = getPost($id); $post->title = $newTitle; $post->save(); 

Por favor, não faça isso. O valor no cache pode estar desatualizado, mesmo se a invalidação for feita corretamente. Uma pequena condição de corrida e publicação perderá as alterações feitas por outro usuário. Bloqueios otimistas ajudarão pelo menos a não perder alterações, mas o número de solicitações erradas pode aumentar bastante.


A melhor solução é usar uma lógica de seleção de entidade completamente diferente para operações de leitura e gravação (Olá, CQRS). Nas operações de gravação, você sempre precisa selecionar o valor mais recente no banco de dados. E não se esqueça dos bloqueios (otimistas ou pessimistas) para dados importantes.


Eu acho que isso é suficiente para um artigo introdutório. O armazenamento em cache é um tópico muito complexo e demorado, com traps para desenvolvedores, mas o ganho de desempenho às vezes supera todas as dificuldades.

Source: https://habr.com/ru/post/pt463495/


All Articles