Comment créer une couche de mise en cache pour votre API Laravel

PHPz
Libérer: 2024-08-10 06:59:32
original
1037 Les gens l'ont consulté

Disons que vous créez une API pour servir certaines données, vous découvrez que les réponses GET sont assez lentes. Vous avez essayé d'optimiser vos requêtes, d'indexer les tables de votre base de données par colonnes fréquemment interrogées et vous n'obtenez toujours pas les temps de réponse souhaités. La prochaine étape à franchir consiste à écrire une couche de mise en cache pour votre API. « Couche de mise en cache » n'est ici qu'un terme sophistiqué pour désigner un middleware qui stocke les réponses réussies dans un magasin à récupération rapide. par ex. Redis, Memcached, etc., puis toute autre demande adressée à l'API vérifie si les données sont disponibles dans le magasin et fournit la réponse.

Conditions préalables

  • Laravel
  • Redis

Avant de commencer

Je suppose que si vous êtes arrivé ici, vous savez comment créer une application Laravel. Vous devez également disposer d’une instance Redis locale ou cloud à laquelle vous connecter. Si vous avez Docker localement, vous pouvez copier mon fichier de composition ici. Aussi, pour un guide sur la façon de se connecter au pilote de cache Redis, lisez ici.

Création de nos données factices

Pour nous aider à voir que notre couche de mise en cache fonctionne comme prévu. bien sûr, nous avons besoin de données, disons que nous avons un modèle nommé Post. je vais donc créer quelques articles, j'ajouterai également un filtrage complexe qui pourrait nécessiter beaucoup de base de données et nous pourrons ensuite optimiser par mise en cache.

Commençons maintenant à écrire notre middleware :

Nous créons notre squelette middleware en exécutant

php artisan make:middleware CacheLayer
Copier après la connexion

Ensuite, enregistrez-le dans votre app/Http/Kernel.php sous le groupe api middleware comme ceci :

    protected $middlewareGroups = [
        'api' => [
            CacheLayer::class,
        ],
    ];
Copier après la connexion

Mais si vous utilisez Laravel 11. enregistrez-le dans votre bootstrap/app.php

->withMiddleware(function (Middleware $middleware) {
        $middleware->api(append: [
            \App\Http\Middleware\CacheLayer::class,
        ]);
    })
Copier après la connexion

Terminologies de mise en cache

  • Cache Hit : se produit lorsque les données demandées sont trouvées dans le cache.
  • Cache Miss : se produit lorsque les données demandées ne sont pas trouvées dans le cache.
  • Cache Flush : effacement des données stockées dans le cache afin qu'elles puissent être à nouveau remplies avec de nouvelles données.
  • Balises de cache : il s'agit d'une fonctionnalité unique à Redis. Les balises de cache sont une fonctionnalité utilisée pour regrouper les éléments associés dans le cache, ce qui facilite la gestion et l'invalidation simultanées des données associées.
  • Durée de vie (TTL) : il s'agit de la durée pendant laquelle un objet mis en cache reste valide avant son expiration. Un malentendu courant consiste à penser que chaque fois qu'un objet est accédé à partir du cache (un accès au cache), son délai d'expiration est réinitialisé. Cependant, ce n'est pas vrai. Par exemple, si la durée de vie est définie sur 5 minutes, l'objet mis en cache expirera après 5 minutes, quel que soit le nombre d'accès au cours de cette période. Une fois les 5 minutes écoulées, la prochaine requête pour cet objet entraînera la création d'une nouvelle entrée dans le cache.

Calcul d'une clé de cache unique

Les pilotes de cache sont donc un magasin clé-valeur. donc vous avez une clé alors la valeur est votre json. Vous avez donc besoin d'une clé de cache unique pour identifier les ressources, une clé de cache unique aidera également à l'invalidation du cache, c'est-à-dire à la suppression des éléments du cache lorsqu'une nouvelle ressource est créée/mise à jour. Mon approche pour la génération de clé de cache consiste à transformer l'URL de la requête, les paramètres de requête et le corps en un objet. puis sérialisez-le en chaîne. Ajoutez ceci à votre middleware de cache :

class CacheLayer 
{
    public function handle(Request $request, Closure $next): Response
    {
    }

    private function getCacheKey(Request $request): string
    {
        $routeParameters = ! empty($request->route()->parameters) ? $request->route()->parameters : [auth()->user()->id];
        $allParameters = array_merge($request->all(), $routeParameters);
        $this->recursiveSort($allParameters);

        return $request->url() . json_encode($allParameters);
    }

    private function recursiveSort(&$array): void
    {
        foreach ($array as &$value) {
            if (is_array($value)) {
                $this->recursiveSort($value);
            }
        }

        ksort($array);
    }
}
Copier après la connexion

Parcourons le code ligne par ligne.

  • Nous vérifions d’abord les paramètres de requête correspondants. nous ne voulons pas calculer la même clé de cache pour /users/1/posts et /users/2/posts.
  • Et s'il n'y a aucun paramètre correspondant, nous transmettons l'identifiant de l'utilisateur. Cette partie est facultative. Si vous avez des itinéraires comme /user qui renvoient les détails de l'utilisateur actuellement authentifié. il conviendra de transmettre l'identifiant de l'utilisateur dans la clé de cache. sinon, vous pouvez simplement en faire un tableau vide ([]).
  • Ensuite, nous obtenons tous les paramètres de requête et les fusionnons avec les paramètres de requête
  • Ensuite, nous trions les paramètres. La raison pour laquelle cette étape de tri est très importante est que nous pouvons renvoyer les mêmes données pour, disons, /posts?page=1&limit=20 et /posts?limit=20&page=1. donc quel que soit l'ordre des paramètres, nous renvoyons toujours la même clé de cache.

Hors itinéraires

Donc, selon la nature de l'application que vous créez. Il y aura certaines routes GET que vous ne souhaitez pas mettre en cache, c'est pourquoi nous créons une constante avec l'expression régulière pour correspondre à ces routes. Cela ressemblera à :

 private const EXCLUDED_URLS = [
    '~^api/v1/posts/[0-9a-zA-Z]+/comments(\?.*)?$~i'
'
];
Copier après la connexion

Dans ce cas, cette expression régulière correspondra à tous les commentaires d'une publication.

Configuration du TTL

Pour cela, ajoutez simplement cette entrée à votre config/cache.php

  'ttl' => now()->addMinutes(5),
Copier après la connexion

Écrire notre middleware

Maintenant que nous avons défini toutes nos étapes préliminaires, nous pouvons écrire notre code middleware :

public function handle(Request $request, Closure $next): Response
    {
        if ('GET' !== $method) {
           return $next($request);
        }

        foreach (self::EXCLUDED_URLS as $pattern) {
            if (preg_match($pattern, $request->getRequestUri())) {
                return $next($request);
            }
        }

        $cacheKey = $this->getCacheKey($request);

        $exception = null;

        $response = cache()
            ->tags([$request->url()])
            ->remember(
                key: $cacheKey,
                ttl: config('cache.ttl'),
                callback: function () use ($next, $request, &$exception) {
                    $res = $next($request);

                    if (property_exists($res, 'exception') && null !== $res->exception) {
                        $exception = $res;

                        return null;
                    }

                    return $res;
                }
            );

        return $exception ?? $response;
    }
Copier après la connexion
  • First we skip caching for non-GET requests and Excluded urls.
  • Then we use the cache helper, tag that cache entry by the request url.
  • we use the remember method to store that cache entry. then we call the other handlers down the stack by doing $next($request). we check for exceptions. and then either return the exception or response.

Cache Invalidation

When new resources are created/updated, we have to clear the cache, so users can see new data. and to do this we will tweak our middleware code a bit. so in the part where we check the request method we add this:

if ('GET' !== $method) {
    $response = $next($request);

    if ($response->isSuccessful()) {
        $tag = $request->url();

        if ('PATCH' === $method || 'DELETE' === $method) {
            $tag = mb_substr($tag, 0, mb_strrpos($tag, '/'));
        }

        cache()->tags([$tag])->flush();
    }

    return $response;
}
Copier après la connexion

So what this code is doing is flushing the cache for non-GET requests. Then for PATCH and Delete requests we are stripping the {id}. so for example if the request url is PATCH /users/1/posts/2 . We are stripping the last id leaving /users/1/posts. this way when we update a post, we clear the cache of all a users posts. so the user can see fresh data.

Now with this we are done with the CacheLayer implementation. Lets test it

Testing our Cache

Let's say we want to retrieve all a users posts, that has links, media and sort it by likes and recently created. the url for that kind of request according to the json:api spec will look like: /posts?filter[links]=1&filter[media]=1&sort=-created_at,-likes. on a posts table of 1.2 million records the response time is: ~800ms

How to build a caching layer for your Laravel API
and after adding our cache middleware we get a response time of 41ms

How to build a caching layer for your Laravel API

Great success!

Optimizations

Another optional step is to compress the json payload we store on redis. JSON is not the most memory-efficient format, so what we can do is use zlib compression to compress the json before storing and decompress before sending to the client.
the code for that will look like:

$response = cache()
            ->tags([$request->url()])
            ->remember(
                key: $cacheKey,
                ttl: config('cache.ttl'),
                callback: function () use ($next, $request, &$exception) {
                    $res = $next($request);

                    if (property_exists($res, 'exception') && null !== $res->exception) {
                        $exception = $res;

                        return null;
                    }

                    return gzcompress($res->getContent());
                }
            );

        return $exception ?? response(gzuncompress($response));
Copier après la connexion

The full code for this looks like:

getMethod();

        if ('GET' !== $method) {
            $response = $next($request);

            if ($response->isSuccessful()) {
                $tag = $request->url();

                if ('PATCH' === $method || 'DELETE' === $method) {
                    $tag = mb_substr($tag, 0, mb_strrpos($tag, '/'));
                }

                cache()->tags([$tag])->flush();
            }

            return $response;
        }

        foreach (self::EXCLUDED_URLS as $pattern) {
            if (preg_match($pattern, $request->getRequestUri())) {
                return $next($request);
            }
        }

        $cacheKey = $this->getCacheKey($request);

        $exception = null;

        $response = cache()
            ->tags([$request->url()])
            ->remember(
                key: $cacheKey,
                ttl: config('cache.ttl'),
                callback: function () use ($next, $request, &$exception) {
                    $res = $next($request);

                    if (property_exists($res, 'exception') && null !== $res->exception) {
                        $exception = $res;

                        return null;
                    }

                    return gzcompress($res->getContent());
                }
            );

        return $exception ?? response(gzuncompress($response));
    }

    private function getCacheKey(Request $request): string
    {
        $routeParameters = ! empty($request->route()->parameters) ? $request->route()->parameters : [auth()->user()->id];
        $allParameters = array_merge($request->all(), $routeParameters);
        $this->recursiveSort($allParameters);

        return $request->url() . json_encode($allParameters);
    }

    private function recursiveSort(&$array): void
    {
        foreach ($array as &$value) {
            if (is_array($value)) {
                $this->recursiveSort($value);
            }
        }

        ksort($array);
    }
}
Copier après la connexion

Summary

This is all I have for you today on caching, Happy building and drop any questions, commments and improvements in the comments!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal