Mise en cache Laravel: les bases plus les trucs et astuces

La technique de mise en cache vous permet de créer des applications plus évolutives, en stockant les résultats de certaines requêtes dans un stockage rapide en mémoire. Cependant, une mise en cache incorrectement implémentée peut dégrader considérablement l'impression de l'utilisateur sur votre application. Cet article contient quelques concepts de base sur la mise en cache, diverses règles et tabous que j'ai appris de plusieurs projets antérieurs.


N'utilisez pas la mise en cache.


Votre projet est-il rapide et n'a-t-il aucun problème de performances?
Oubliez la mise en cache. Sérieusement :)


Cela compliquera considérablement les opérations de lecture à partir de la base de données sans aucun avantage.


Certes, Mohamed Said au début de cet article fait quelques calculs et prouve que dans certains cas, l'optimisation de l'application en millisecondes peut économiser une tonne d'argent sur votre compte AWS. Donc, si les économies prévues sur votre projet dépassent 1,86 $, la mise en cache est peut-être une bonne idée.


Comment ça marche?


Lorsqu'une application souhaite obtenir des données de la base de données, par exemple, l'entité Post par son identifiant, elle génère une clé de mise en cache unique pour ce cas ( 'post_' . $id est tout à fait appropriée) et essaie de trouver la valeur par cette clé dans le stockage de valeur-clé rapide (memcache, redis, ou autre). Si la valeur est là, l'application l'utilise. Sinon, il le prend dans la base de données et le stocke dans le cache par cette clé pour une utilisation future.



Garder cette valeur dans le cache n'est pas une bonne idée pour toujours, car cette entité Post peut être mise à jour, mais l'application recevra toujours l'ancienne valeur mise en cache.
Par conséquent, les fonctions de mise en cache demandent généralement à quelle heure cette valeur doit être stockée.


Passé ce délai, memcache ou redis «l'oubliera» et l'application prendra une nouvelle valeur dans la base de données.


Un exemple:


 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; } 

Ici, je mets l'entité Post dans le cache pendant 15 minutes (depuis la version 5.8, laravel utilise des secondes dans ce paramètre, avant qu'il y ait des minutes). La façade Cache a également une méthode de remember pratique pour ce cas. Ce code fait exactement la même chose que le précédent:


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

Il existe un chapitre Cache dans la documentation Laravel qui explique comment installer les pilotes nécessaires pour votre application et les fonctionnalités principales.


Données mises en cache


Tous les pilotes Laravel standard stockent les données sous forme de chaînes. Lorsque nous vous demandons de mettre en cache une instance du modèle Eloquent, il utilise la fonction sérialiser pour obtenir la chaîne de l'objet. La fonction unserialize restaure l'état d'un objet lorsque nous l'obtenons du cache.


Presque toutes les données peuvent être mises en cache. Nombres, chaînes, tableaux, objets (s'ils peuvent être correctement sérialisés, voir les descriptions des fonctions des liens plus tôt).


Les entités et les collections éloquentes peuvent être facilement mises en cache et sont les valeurs les plus populaires dans le cache d'application Laravel. Cependant, l'utilisation d'autres types est également pratiquée assez largement. La méthode Cache::increment est populaire pour implémenter différents compteurs. En outre, les verrous atomiques sont très utiles lorsque les développeurs luttent contre les conditions de concurrence .


Que mettre en cache?


Les premiers candidats à la mise en cache sont les requêtes qui sont exécutées très souvent, mais leur plan d'exécution n'est pas le plus simple. Le meilleur exemple est le top 5 des articles sur la page principale ou les dernières nouvelles. La mise en cache de ces valeurs peut améliorer considérablement les performances de la page principale.


Habituellement, la récupération d'entités par id en utilisant Model::find($id) est très rapide, mais si cette table est lourdement chargée avec de nombreuses requêtes de mise à jour, d'insertion et de suppression, la réduction du nombre de requêtes sélectionnées donnera un bon répit à la base de données. Les entités avec des relations hasMany qui se chargeront à chaque fois sont également de bons candidats pour la mise en cache. Lorsque j'ai travaillé sur un projet avec plus de 10 millions de visiteurs par jour, nous avons mis en cache presque toutes les demandes sélectionnées.


Invalidation du cache


La décomposition des clés après un temps spécifié aide à mettre à jour les données dans le cache, mais cela ne se produit pas immédiatement. L'utilisateur peut modifier les données, mais pendant un certain temps, il continuera d'en voir l'ancienne version dans l'application. Le dialogue habituel sur l'un de mes projets passés:


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

Ce comportement est très gênant pour les utilisateurs, et la décision évidente de supprimer les anciennes données du cache lorsque nous les avons mises à jour vient rapidement à l'esprit. Ce processus est appelé invalidité. Pour des clés simples comme "post_%id%" , l' "post_%id%" pas très difficile.


Des événements éloquents peuvent aider, ou si votre application génère des événements spéciaux tels que PostPublished ou UserBanned cela peut être encore plus simple. Exemple avec des événements éloquents. Vous devez d'abord créer des classes d'événements. Pour plus de commodité, je vais utiliser une classe abstraite pour eux:


 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{} 

Bien sûr, selon PSR-4, chaque classe doit être dans son propre fichier. Configurer la classe Post Eloquent (en utilisant la documentation ):


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

Créez un écouteur pour ces événements:


 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); } } 

Ce code supprimera les valeurs mises en cache après chaque mise à jour ou suppression d'entités Post. L'invalidation des listes d'entités, comme les 5 premiers articles ou les dernières nouvelles, sera un peu plus compliquée. J'ai vu trois stratégies:


Ne pas désactiver la stratégie


Ne touchez tout simplement pas à ces valeurs. Habituellement, cela ne pose aucun problème. Il est normal que les nouvelles nouvelles apparaissent dans la liste de ces dernières un peu plus tard (bien sûr, s'il ne s'agit pas d'un grand portail d'actualités). Mais pour certains projets, il est vraiment important d'avoir de nouvelles données dans ces listes.


Trouver et désamorcer la stratégie


Chaque fois que vous mettez à jour une publication, vous pouvez essayer de la trouver dans les listes mises en cache et si elle s'y trouve, supprimez cette valeur mise en 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; } } } } 

Ça a l'air moche, mais ça marche.


Stratégie "identifiant de magasin"


Si l'ordre des éléments de la liste est sans importance, seul l'identifiant des entrées peut être stocké dans le cache. Après avoir reçu l'id, vous pouvez créer une liste de clés de la forme 'post_'.$id et obtenir toutes les valeurs en utilisant la méthode Cache::many , qui obtient beaucoup de valeurs du cache en une seule demande (cela est aussi appelé multi get).


L'invalidation du cache n'est pas en vain appelée l'une des deux difficultés de programmation et est très difficile dans certains cas.


Mise en cache des relations


La mise en cache d'entités avec des relations nécessite une attention accrue.


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

Ce code effectue deux requêtes SELECT . Obtention de l'entité par id et des commentaires par post_id . Nous implémentons la mise en 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...) 

La première demande a été mise en cache et la seconde ne l'a pas été. Lorsque le pilote de cache écrit Post dans le cache, les comments ne sont pas encore chargés. Si nous voulons aussi les mettre en cache, nous devons les charger manuellement:


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

Les deux requêtes sont maintenant mises en cache, mais nous devons invalider les valeurs de 'post_'.$id chaque fois qu'un commentaire est ajouté. Ce n'est pas très efficace, il est donc préférable de stocker le cache de commentaires séparément:


 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...) 

Parfois, l'essence et l'attitude sont fortement liées les unes aux autres et sont toujours utilisées ensemble (ordre avec détails, publication avec traduction dans la langue souhaitée). Dans ce cas, les stocker dans un cache est tout à fait normal.


Source unique de vérité pour les clés de cache


Si le projet implémente l'invalidation, les clés de cache sont générées à au moins deux endroits: pour appeler Cache::get / Cache::remember et pour appeler Cache::forget . J'ai déjà rencontré des situations où cette clé a été changée à un endroit, mais pas à un autre, et le handicap a éclaté. Le conseil habituel pour de tels cas est les constantes, mais les clés de cache sont générées dynamiquement, donc j'utilise des classes spéciales qui génèrent des clés:


 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)); 

Les durées de vie clés peuvent également être rendues en constantes pour une meilleure lisibilité. Ces 900 ou 15 * 60 augmentent la charge cognitive lors de la lecture du code.


N'utilisez pas le cache dans les opérations d'écriture


Lors de la mise en œuvre d'opérations d'écriture, telles que la modification du titre ou du texte d'une publication, il est tentant d'utiliser la méthode getPost écrite précédemment:


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

Veuillez ne pas le faire. La valeur dans le cache peut être obsolète, même si l'invalidation est effectuée correctement. Une petite condition de concurrence critique et la publication perdront les modifications apportées par un autre utilisateur. Les verrous optimistes aideront au moins à ne pas perdre les modifications, mais le nombre de demandes erronées peut considérablement augmenter.


La meilleure solution consiste à utiliser une logique de sélection d'entité complètement différente pour les opérations de lecture et d'écriture (bonjour, CQRS). Dans les opérations d'écriture, vous devez toujours sélectionner la dernière valeur dans la base de données. Et n'oubliez pas les verrous (optimistes ou pessimistes) pour les données importantes.


Je pense que cela suffit pour un article d'introduction. La mise en cache est un sujet très complexe et long, avec des pièges pour les développeurs, mais le gain de performances l'emporte parfois sur toutes les difficultés.

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


All Articles