如何為 Laravel API 建立緩存層

PHPz
發布: 2024-08-10 06:59:32
原創
1038 人瀏覽過

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

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板