首页  >  文章  >  后端开发  >  如何为 Laravel API 构建缓存层

如何为 Laravel API 构建缓存层

PHPz
PHPz原创
2024-08-10 06:59:321017浏览

假设您正在构建一个 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: [
            \App\Http\Middleware\CacheLayer::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;
    }
  • 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;
}

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));

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);
    }
}

Summary

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中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn