假设您正在构建一个 api 来提供一些数据,您发现 get 响应非常慢。您已尝试优化查询,通过频繁查询的列对数据库表建立索引,但仍然没有获得所需的响应时间。下一步是为您的 api 编写一个缓存层。这里的“缓存层”只是中间件的一个奇特术语,它将成功的响应存储在快速检索存储中。例如redis、memcached 等,然后对 api 的任何进一步请求都会检查数据是否在存储中可用并提供响应。
先决条件
- 拉拉维尔
- redis
在我们开始之前
我假设如果您已经到达这里,您就知道如何创建 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: [ apphttpmiddlewarecachelayer::class, ]); })
缓存术语
- 缓存命中:当在缓存中找到请求的数据时发生。
- cache miss:当请求的数据在缓存中找不到时发生。
- 缓存刷新:清除缓存中存储的数据,以便可以用新数据重新填充。
- 缓存标签:这是redis独有的功能。缓存标签是一种用于对缓存中的相关项目进行分组的功能,可以更轻松地同时管理和使相关数据失效。
- 生存时间(ttl):这是指缓存对象在过期之前保持有效的时间。一种常见的误解是认为每次从缓存访问对象(缓存命中)时,其过期时间都会重置。然而,事实并非如此。例如,如果 ttl 设置为 5 分钟,则缓存对象将在 5 分钟后过期,无论在该时间内被访问多少次。 5 分钟结束后,对该对象的下一个请求将导致在缓存中创建一个新条目。
计算唯一的缓存键
所以缓存驱动程序是一个键值存储。所以你有一个键,那么值就是你的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); } }
让我们逐行查看代码。
- 首先我们检查是否有匹配的请求参数。我们不想为 /users/1/posts 和 /users/2/posts 计算相同的缓存键。
- 如果没有匹配的参数,我们传入用户的 id。这部分是可选的。如果您有类似 /user 的路由,它返回当前经过身份验证的用户的详细信息。在缓存键中传入用户 id 是合适的。如果不是,你可以将其设置为空数组([])。
- 然后我们获取所有查询参数并将其与请求参数合并
- 然后我们对参数进行排序,为什么这个排序步骤非常重要,因为这样我们就可以返回相同的数据,例如 /posts?page=1&limit=20 和 /posts?limit=20&page=1。因此,无论参数的顺序如何,我们仍然返回相同的缓存键。
排除航线
所以取决于您正在构建的应用程序的性质。会有一些您不想缓存的 get 路由,因此我们使用正则表达式创建一个常量来匹配这些路由。这看起来像:
private const excluded_urls = [ '~^api/v1/posts/[0-9a-za-z]+/comments(?.*)?$~i' ' ];
在这种情况下,这个正则表达式将匹配所有帖子的评论。
配置ttl
为此,只需将此条目添加到您的 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; }
- 首先,我们跳过非 get 请求和排除网址的缓存。
- 然后我们使用缓存助手,通过请求 url 标记该缓存条目。
- 我们使用 remember 方法来存储该缓存条目。然后我们通过 $next($request) 调用堆栈中的其他处理程序。我们检查是否有异常。然后返回异常或响应。
缓存失效
当新资源创建/更新时,我们必须清除缓存,以便用户可以看到新数据。为此,我们将稍微调整我们的中间件代码。所以在我们检查请求方法的部分我们添加这个:
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; }
所以这段代码所做的是刷新非 get 请求的缓存。然后,对于 patch 和删除请求,我们将剥离 {id}。例如,如果请求 url 是 patch /users/1/posts/2 。我们正在删除最后一个 id,留下 /users/1/posts。这样,当我们更新帖子时,我们会清除所有用户帖子的缓存。这样用户就可以看到最新的数据。
现在我们已经完成了 cachelayer 的实现。来测试一下吧
测试我们的缓存
假设我们想要检索所有包含链接、媒体的用户帖子,并按喜欢和最近创建的内容对其进行排序。根据 json:api 规范,此类请求的 url 如下所示:/posts?filter[links]=1&filter[media]=1&sort=-created_at,-likes。在包含 120 万条记录的帖子表上,响应时间为:~800ms
添加缓存中间件后,我们的响应时间为 41 毫秒
非常成功!
优化
另一个可选步骤是压缩我们存储在 redis 上的 json 负载。 json 不是最节省内存的格式,所以我们可以做的是在存储之前使用 zlib 压缩来压缩 json,并在发送到客户端之前解压。
其代码如下所示:
$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));
完整代码如下所示:
<?php namespace AppHttpMiddleware; use Closure; use IlluminateHttpRequest; use SymfonyComponentHttpFoundationResponse; class CacheLayer { private const EXCLUDED_URLS = []; public function handle(Request $request, Closure $next): Response { $method = $request->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); } }
概括
这就是我今天为您提供的有关缓存的全部内容,祝您构建愉快,并在评论中提出任何问题、意见和改进!
以上就是如何为 Laravel API 构建缓存层的详细内容,更多请关注php中文网其它相关文章!