Laravel 11'de Flexible Cache ve Redis Kullanımı

229

Ağustos 2024'ün sonunda Amerika, Teksas'ta Laracon US yapıldı. İki günlük programın YouTube kayıtlarını bu linkten izleyebilirsiniz. Taylor Otwell'in yeni versiyona dair yenilikleri anlattığı konulardan birisi de cache sisteminde yaptıkları yeniliklerden birisi olan Flexible fonksiyonuydu. Ben de bu yeniliği çok beğendim ve dilim döndüğünce size de anlatmak istedim.

Cache sistemleri herkesin kullandığı ya da kullanmak zorunda kaldığı bir şey değil. O yüzden olası bir kullanım senaryosuyla anlatmak daha iyi olabilir. Ürünlerinizin kaydedildiği bir depo yönetim sistemi yazdığınızı varsayalım. Yazdığınız uygulama, uygulamayı ayağa kaldırdığınız ilk zamanlarda oldukça hızlı çalışıyor olabilir. Zaten beklenen de budur. Yalnız ürün sayısı artınca (8-10bin kayıttan bahsetmiyorum, 100binler ve üstündeki sayılardan...) sistemin zamanla nasıl yavaşladığını göreceksiniz. Bunun kullanılan yazılım ya da dilden ziyade eşyanın tabiatı dolayısıyla gerçekleştiğini bilmenizde fayda var.

Örneğin bir ürünün özellikleri her gün değişmediği için çok çok çok büyük ihtimalle her ürünü sonsuz zamanlı (TTL, time to live) bir cache'e alınabilir. Dolayısıyla ürünü görüntülemek isteyen ziyaretçi, ürünü ilk açan ziyaretçinin cache'e aldığı sayfayı/veritabanı sorgusunu görüntüleyecektir. Ürünle ilgili değişen bir şey olmadığı için bu bir problem değil, aksine veritabanı yükünü azaltan bir çözüm olacaktır. Ayrıca görüntülenen sayfanın tamamını cache'e almak zorunda değilsiniz. Belli başlı sorguları cache'e alsanız yeter.

Ayrı bir sayfamızda da bütün ürünlerin id'sini, adını, üreticisini, KDV oranını, stok bilgisini vs. görüntülediğimiz bir tablomuz, ya da API endpoint'imiz olsun. Ve yine bu listemiz oldukça büyük bir liste/tablo/JSON olsun. Sebebi ne olursa olsun böyle bir listeye ihtiyacımız olduğunu varsayalım. (Bu arada 200bin satırla işlem yapmak zorunda kaldığımız projelerden birinde çalışmıştım, olmaz olmaz demeyin.) Böyle bir tablonun derlenip kullanıcıya gösterilmesinin kaç saniye süreceğini ve kaç kullanıcının aynı tabloya erişmek istediğini bir düşünün. Bunu gerçek zamanlı olarak sunucudan alıp kullanıcıya sunmak çok büyük bir sunucu masrafına sebep olacaktır.

Bu ve buna benzer durumlardaki sıkıntıları aşmaktaki yöntemlerden birisi, istenen bazı bilgileri cache'e almaktır. Böylece her sorguyu veritabanında yapmanıza gerek kalmayacak ve normal sorgulara göre çok çok hızlı yanıtlar alabileceksiniz. Laravel'de birçok cache sürücüsü varsayılan olarak desteklemekte. Bunlardan birkaçı aşağıdaki gibi:

  • Database
  • Memcached
  • Redis
  • File
  • Octane vs...

Bu yazının başlığında görüldüğü üzere ben Redis'ten bahsetmek istiyorum. Redis (Remote Dictionary Server), açık kaynaklı, bellek tabanlı bir veri deposudur. Verileri anahtar-değer çiftleri şeklinde saklar ve bu sayede çok hızlı erişim sağlar. Bu özelliği sayesinde genellikle web uygulamalarında önbellekleme, oturum yönetimi, gerçek zamanlı verilerin depolanması gibi alanlarda kullanılır. Verileri RAM'de tuttuğu için disk erişimlerinin yavaşlığından etkilenmez ve bu sayede çok hızlı okuma ve yazma işlemleri gerçekleştirir. Her verinin bir anahtarı vardır, her anahtara karşılık bir veri vardır. İsterseniz 50bin satırlık işlem tablosu, isterseniz 4 satırlık blog yazısını bellekte tutabilirsiniz. Laravel için kurulumu da kullanımı da oldukça kolaydır ayrıca. Windows, Linux ve macOS için yüklemek istiyorsanız bu linki inceleyebilirsiniz.

Ubuntu Sunucularda Redis Kurulumu

Paket yöneticisini APT indeksinize ekleyin, indeksi güncelleyin ve Redis'i yükleyin.

sudo apt-get install lsb-release curl gpg
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
sudo chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt-get update
sudo apt-get install redis

Yükleme işleminden sonra Redis otomatik olarak çalışacaktır ve olası bir restart işleminden sonra yeniden başlatılacaktır. Redis'in yüklemesinin tamamı bu. :)

Yüklemeden sonra Laravel'e cache sürücüsü ve deposu olarak Redis'i kullanmak istediğimizi söylememiz lazım. Gel gelelim Laravel ayarlarına:

Öncelikle PHP için redis eklentisini yüklememiz gerekiyor. Ubuntu için aşağıdaki komutu girip yüklemeyi yapabilirsiniz.

sudo apt install php8.3-redis

Eğer siz farklı bir PHP versiyonu kullanıyorsanız kodu o versiyona göre güncellemeyi unutmayın. Yükleme işlemi tamamlandıktan sonra, kullandığınız web sunucusunu yeniden başlatmanız gerekir.

Apache için:

sudo systemctl restart apache2

Nginx için:

sudo systemctl restart nginx

Eğer olur da bu eklentiyi yüklemenize engel bir durum varsa ayrıca tamamen PHP ile yazılmış predis paketini yükleyerek de Redis'i kullanabilirsiniz.

composer require predis/predis:^2.0

PHP Redis eklentisini ya da predis paketini yükledikten sonra gereken parametreleri Laravel'e girmeniz gerekiyor. Öncelikle config/database.php içindeki ayarlamaları yapalım.

'redis' => [

        'client' => env('REDIS_CLIENT', 'predis'),

        'options' => [
            'cluster' => env('REDIS_CLUSTER', 'redis'),
            'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
        ],

        'default' => [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'username' => env('REDIS_USERNAME', 'muratgokoglu'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', '6379'),
            'database' => env('REDIS_DB', '0'),
        ],

        'cache' => [
            'url' => env('REDIS_URL'),
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'username' => env('REDIS_USERNAME'),
            'password' => env('REDIS_PASSWORD'),
            'port' => env('REDIS_PORT', '6379'),
            'database' => env('REDIS_CACHE_DB', '1'),
        ],

    ],

Ayrıca .env içindeki anahtarlarımızı da girelim. Yoksa yukarıda da gördüğünüz üzere Laravel varsayılan ayarları kullanacaktır. 

CACHE_STORE=redis
CACHE_DRIVER=redis

REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Ayarlamalar tamam. Artık Laravel uygulamamız için varsayılan cache sürücüsü ve deposu Redis oldu. Test için de bir raporlar tablosu oluşturdum. 5550 dummy kaydı da factory yazarak seed ettim. Tabloya da 8 tane relation yazdım ki yine de yukarıda sözünü ettiğim rakamlar için yük bile sayılmaz bu sayılar.

1,87 saniyede yüklenmiş ki burada sanırım bir ya da iki sorgu cache'li olarak yüklenmiş. Çünkü modellerin 9100 eleman dödürmesi lazım, burada dikkat ederseniz 7531 tane yüklenmiş. Biraz torpil geçtik anlayacağınız.

Ve bu da Redis ile cache' aldığımız aynı model ve sorguların dönüş hızı. 182ms ile 10 kata varan bir fark oluşmuş. Bu fark genelde lineer oluşmuyor ama. O yüzden kendi denemelerinizi yapıp uygun sorgu yapısını bulmanızı tavsiye ederim.

Controller kodları da aşağıdaki gibi:

//Herhangi bir önbellekleme işlemi yapılmadan sorgularımızı yapıyoruz.
$reports = Report::with(
            'user:id,name,lastname',
            'project:id,name',
            'customer:id,name',
            'inspection_type:id,name',
            'inspection_status:id,name',
            'location:id,name',
            'inventory:id,name',
            'standard:id,name',
        )->orderByDesc('id')->get();

        $projects = Project::select('id', 'name')->get();
        $users = User::select('id', 'name', 'lastname')->get();
        $customers = Customer::select('id', 'name')->get();
        $inventories = Inventory::select('id', 'name')->get();
        $locations = Location::select('id', 'name')->get();
        $standards = Standard::select('id', 'name')->get();
        $inspection_types = InspectionType::all();
        $inspection_statuses = InspectionStatus::all();
//Hem Redis ile cache'e alıyoruz hem de Laravel 11 ile gelen Flexible fonksiyonunu kullanıyoruz.
        $reports = Redis::get('reports_data');

        if (!$reports) {
            $reports = Report::with(
                'user:id,name,lastname',
                'project:id,name',
                'customer:id,name',
                'inspection_type:id,name',
                'inspection_status:id,name',
                'location:id,name',
                'inventory:id,name',
                'standard:id,name',
            )->orderByDesc('id')->get();
            Redis::setex('reports_data', 100, gzcompress(json_encode($reports)));
        }
        else{
            $reports = json_decode(gzuncompress($reports));
        }


        $projects = Cache::flexible('projects_flex', [60, 120], function () {
            return Project::select('id', 'name')->get();
        });

        $users = Cache::flexible('users_flex', [60, 120], function () {
            return User::select('id', 'name', 'lastname')->get();
        });

        $customers = Cache::flexible('customers_flex', [60, 120], function () {
            return Customer::select('id', 'name')->get();
        });

        $inventories = Cache::flexible('inventories_flex', [60, 120], function () {
            return Inventory::select('id', 'name')->get();
        });

        $locations = Cache::flexible('locations_flex', [60, 120], function () {
            return Location::select('id', 'name')->get();
        });

        $standards = Cache::flexible('standards_flex', [60, 120], function () {
            return Standard::select('id', 'name')->get();
        });

        $inspection_types = Cache::flexible('inspection_types_flex', [60, 120], function () {
            return InspectionType::all();
        });
        $inspection_statuses = Cache::flexible('inspection_statuses_flex', [60, 120], function () {
            return InspectionStatus::all();
        });

Yukarıdaki kodlarda ilk önce Redis'te istediğimiz key'in olup olmadığını kontrol ediyoruz. Eğer key varsa ne ala, al onu kullan diyoruz. Yok eğer key yoksa onu ilk başta istediğimiz sorguyla oluşturuyoruz, biraz daha performans istediğimiz için de sıkıştırarak depoluyoruz. Sıkıştırmada gönderilen paketin küçüklüğünün hız avantajı oluyor ama sıkıştırılmış veriyi açarken sunucunun işlemci yükünü bir miktar arttırmış oluyoruz. Seçim size ait, sıkıştırmadan da kullanmak mümkün ki altındaki sorgularda gördüğünüz üzere herhangi bir sıkıştırma işlemi yapılmıyor.

Yalnız dikkatinizi çektiyse direkt olarak Cache kullanmıyoruz. Flexible fonsiyonuyla bunu kullanıyoruz. Flexible cache sisteminin avantajını anlatmak için önce geleneksek cache sisteminden biraz bahsetmem gerekiyor.

Normalde bir veriyi cache'e aldığınız zaman, o cache'in ne kadar süreyle geçerli olduğuna dair bir bilgi girmeniz gerekir. 5 dakika, 1 saat, 3 gün ya da sürekli gibi... Görüntülenmek istenen veri 5 dakikalık bir süreyle bellekte tutuluyorsa, 5 dakika dolduğu zaman kullanıcı sayfayı görüntülediğinde sayfa artık cache'te olmadığı için yine sistemin aynı veriyi üretip cache'e atması gerekecek. Bu da kullanıcının yine o süreyi beklemesi anlamına geliyor. Ama Flexible fonsiyonunda ise 5 ile 10 dakikalık bir aralık belirlenmişse, 7 dakikada sisteme giren kullanıcı için 5 dakikalık cache verisi gönderilir ve kullanıcı süre dolduğu için yine sorguların yapılmasını beklemez. Dolayısıyla kullanım senaryonuza göre taze veri süre aralığını, kullanıcılarınızın kullanım deneyimine göre belirleyip kabul edilebilir tazelikte verileri kendisine sunarak o beklemeyi ortadan kaldırabilirsiniz.

Böyle bir sistemin Laravel'e gelmesi bizi böyle bir durumu düşünüp buna uygun kodları yazma zahmetinden kurtardı. O yüzden siz de buna benzer performans sorunlarıyla karşılaşmak istemiyorsanız mutlaka birkaç sorgunuzda cache sistemini deneyin. Ne kadar fark ettiğini yukarıdaki görsellerden de anlayacağınız üzere siz de göreceksiniz.