假設您正在建立一個 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中文網其他相關文章!