Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Cara membina lapisan caching untuk API Laravel anda

Cara membina lapisan caching untuk API Laravel anda

PHPz
PHPzasal
2024-08-10 06:59:32830semak imbas

Katakan anda sedang membina API untuk menyampaikan beberapa data, anda dapati respons GET agak perlahan. Anda telah mencuba mengoptimumkan pertanyaan anda, mengindeks jadual pangkalan data anda dengan lajur yang sering ditanya dan anda masih tidak mendapat masa respons yang anda inginkan. Langkah seterusnya yang perlu diambil ialah menulis lapisan Caching untuk API anda. 'Lapisan cache' di sini hanyalah istilah mewah untuk perisian tengah yang menyimpan respons yang berjaya dengan pantas untuk mendapatkan semula kedai. cth. Redis, Memcached dsb. kemudian sebarang permintaan lanjut kepada API menyemak sama ada data tersedia di kedai dan memberikan respons.

Prasyarat

  • Laravel
  • Redis

Sebelum kita mulakan

Saya andaikan jika anda telah sampai di sini, anda tahu cara membuat aplikasi laravel. Anda juga harus mempunyai sama ada contoh Redis tempatan atau awan untuk disambungkan. Jika anda mempunyai docker tempatan, anda boleh menyalin fail karang saya di sini. Juga, untuk panduan tentang cara menyambung kepada pemacu cache Redis baca di sini.

Mencipta Data Dummy kami

Untuk membantu kami melihat lapisan caching kami berfungsi seperti yang diharapkan. sudah tentu kita memerlukan beberapa data, katakan kita mempunyai model bernama Post. jadi saya akan membuat beberapa siaran, saya juga akan menambah beberapa penapisan kompleks yang boleh menjadi intensif pangkalan data dan kemudian kita boleh mengoptimumkan dengan caching.

Sekarang mari mula menulis perisian tengah kami:

Kami mencipta rangka middleware kami dengan menjalankan

php artisan make:middleware CacheLayer

Kemudian daftarkannya dalam apl/Http/Kernel.php anda di bawah kumpulan perisian tengah api seperti:

    protected $middlewareGroups = [
        'api' => [
            CacheLayer::class,
        ],
    ];

Tetapi jika anda menjalankan Laravel 11. daftarkannya dalam bootstrap/app.php anda

->withMiddleware(function (Middleware $middleware) {
        $middleware->api(append: [
            \App\Http\Middleware\CacheLayer::class,
        ]);
    })

Terminologi Caching

  • Cache Hit: berlaku apabila data yang diminta ditemui dalam cache.
  • Cache Miss: berlaku apabila data yang diminta tidak ditemui dalam cache.
  • Cache Flush: mengosongkan data yang disimpan dalam cache supaya ia boleh diisi semula dengan data baharu.
  • Teg cache: Ini adalah ciri unik untuk Redis. tag cache ialah ciri yang digunakan untuk mengumpulkan item berkaitan dalam cache, menjadikannya lebih mudah untuk mengurus dan membatalkan data berkaitan secara serentak.
  • Masa untuk Hidup (TTL): ini merujuk kepada jumlah masa objek cache kekal sah sebelum tamat tempoh. Satu salah faham biasa ialah memikirkan bahawa setiap kali objek diakses daripada cache (cache hit), masa tamatnya ditetapkan semula. Walau bagaimanapun, ini tidak benar. Contohnya, jika TTL ditetapkan kepada 5 minit, objek cache akan tamat tempoh selepas 5 minit, tidak kira berapa kali ia diakses dalam tempoh tersebut. Selepas 5 minit tamat, permintaan seterusnya untuk objek itu akan menyebabkan entri baharu dibuat dalam cache.

Mengira Kunci Cache Unik

Jadi pemacu cache ialah stor nilai kunci. jadi anda mempunyai kunci maka nilainya ialah json anda. Oleh itu, anda memerlukan kunci cache unik untuk mengenal pasti sumber, kunci cache unik juga akan membantu dalam ketidaksahihan cache iaitu mengalih keluar item cache apabila sumber baharu dibuat/kemas kini. Pendekatan saya untuk penjanaan kunci cache ialah menukar url permintaan, parameter pertanyaan dan badan menjadi objek. kemudian sirikannya kepada rentetan. Tambahkan ini pada perisian tengah cache anda:

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

Mari kita lihat baris demi baris.

  • Mula-mula kami menyemak parameter permintaan yang dipadankan. kami tidak mahu mengira kunci cache yang sama untuk /users/1/posts dan /users/2/posts.
  • Dan jika tiada parameter yang sepadan, kami masukkan dalam id pengguna. Bahagian ini adalah pilihan. Jika anda mempunyai laluan seperti /user yang mengembalikan butiran untuk pengguna yang sedang disahkan. ia sesuai untuk memasukkan id pengguna dalam kunci cache. jika tidak, anda boleh menjadikannya tatasusunan kosong([]).
  • Kemudian kami mendapat semua parameter pertanyaan dan menggabungkannya dengan parameter permintaan
  • Kemudian kami mengisih parameter, mengapa langkah pengisihan ini sangat penting supaya kami dapat mengembalikan data yang sama untuk katakan /posts?page=1&limit=20 dan /posts?limit=20&page=1. jadi tanpa mengira susunan parameter kami masih mengembalikan kunci cache yang sama.

Tidak termasuk laluan

Jadi bergantung pada sifat aplikasi yang anda sedang bina. Akan ada beberapa laluan GET yang anda tidak mahu cache jadi untuk ini kami mencipta pemalar dengan regex untuk memadankan laluan tersebut. Ini akan kelihatan seperti:

 private const EXCLUDED_URLS = [
    '~^api/v1/posts/[0-9a-zA-Z]+/comments(\?.*)?$~i'
'
];

Dalam kes ini, regex ini akan sepadan dengan semua ulasan siaran.

Mengkonfigurasi TTL

Untuk ini, tambahkan entri ini pada config/cache.php anda

  'ttl' => now()->addMinutes(5),

Menulis Middleware kami

Sekarang kami telah menetapkan semua langkah awal kami, kami boleh menulis kod perisian tengah kami:

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!

Atas ialah kandungan terperinci Cara membina lapisan caching untuk API Laravel anda. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel sebelumnya:. Petak Ajaib Dalam GridArtikel seterusnya:. Petak Ajaib Dalam Grid