php中文网

如何为 Laravel API 构建缓存层

php中文网

假设您正在构建一个 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-&gt;isSuccessful()) {
                $tag = $request-&gt;url();

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

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

            return $response;
        }

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

        $cacheKey = $this-&gt;getCacheKey($request);

        $exception = null;

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

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

                        return null;
                    }

                    return gzcompress($res-&gt;getContent());
                }
            );

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

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

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

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

        ksort($array);
    }
}

概括

这就是我今天为您提供的有关缓存的全部内容,祝您构建愉快,并在评论中提出任何问题、意见和改进!

以上就是如何为 Laravel API 构建缓存层的详细内容,更多请关注php中文网其它相关文章!