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.
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.
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
Ensuite, enregistrez-le dans votre app/Http/Kernel.php sous le groupe api middleware comme ceci :
protected $middlewareGroups = [ 'api' => [ CacheLayer::class, ], ];
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, ]); })
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); } }
Parcourons le code ligne par ligne.
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' ' ];
Dans ce cas, cette expression régulière correspondra à tous les commentaires d'une publication.
Pour cela, ajoutez simplement cette entrée à votre config/cache.php
'ttl' => now()->addMinutes(5),
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; }
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; }
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
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
and after adding our cache middleware we get a response time of 41ms
Great success!
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));
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); } }
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!