假设您正在构建一个 API 来提供一些数据,您发现 GET 响应非常慢。您已尝试优化查询,通过频繁查询的列对数据库表建立索引,但仍然没有获得所需的响应时间。下一步是为您的 API 编写一个缓存层。这里的“缓存层”只是中间件的一个奇特术语,它将成功的响应存储在快速检索存储中。例如Redis、Memcached 等,然后对 API 的任何进一步请求都会检查数据在存储中是否可用并提供响应。
我假设如果您已经到达这里,您就知道如何创建 Laravel 应用程序。您还应该有一个本地或云 Redis 实例可供连接。如果你本地有 docker,你可以在这里复制我的 compose 文件。另外,有关如何连接到 Redis 缓存驱动程序的指南,请阅读此处。
帮助我们查看缓存层是否按预期工作。当然,我们需要一些数据,假设我们有一个名为 Post 的模型。所以我将创建一些帖子,我还将添加一些可能是数据库密集型的复杂过滤,然后我们可以通过缓存进行优化。
现在让我们开始编写中间件:
我们通过运行创建中间件骨架
php artisan make:middleware CacheLayer
然后将其注册到 api 中间件组下的 app/Http/Kernel.php 中,如下所示:
protected $middlewareGroups = [ 'api' => [ CacheLayer::class, ], ];
但是如果你运行的是 Laravel 11,请在 bootstrap/app.php 中注册它
->withMiddleware(function (Middleware $middleware) { $middleware->api(append: [ \App\Http\Middleware\CacheLayer::class, ]); })
所以缓存驱动程序是一个键值存储。所以你有一个键,那么值就是你的json。因此,您需要一个唯一的缓存键来标识资源,唯一的缓存键还有助于缓存失效,即在创建/更新新资源时删除缓存项。我的缓存键生成方法是将请求 url、查询参数和正文转换为对象。然后将其序列化为字符串。将其添加到您的缓存中间件中:
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); } }
让我们逐行浏览一下代码。
所以取决于您正在构建的应用程序的性质。会有一些您不想缓存的 GET 路由,因此我们使用正则表达式创建一个常量来匹配这些路由。这看起来像:
private const EXCLUDED_URLS = [ '~^api/v1/posts/[0-9a-zA-Z]+/comments(\?.*)?$~i' ' ];
在这种情况下,这个正则表达式将匹配所有帖子的评论。
为此,只需将此条目添加到您的 config/cache.php
'ttl' => now()->addMinutes(5),
现在我们已经设置了所有初步步骤,我们可以编写中间件代码:
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!
以上是如何为 Laravel API 构建缓存层的详细内容。更多信息请关注PHP中文网其他相关文章!