Kenalan dengan Cache::flexible: Cache yang Boleh Basi Sebentar

Abd. Asis
Kenalan dengan Cache::flexible: Cache yang Boleh Basi Sebentar

Ada satu jenis halaman yang hampir selalu lambat di aplikasi: halaman dengan banyak filter. Pengguna memilih merek, transmisi, rentang harga, lalu menekan terapkan. Setiap kombinasi menjalankan satu query yang menyapu ratusan ribu baris.

Selama trafiknya sepi, tidak ada yang mengeluh. Begitu halaman itu dibuka beberapa orang bersamaan, waktu muatnya merangkak ke dua puluh detik, dan Cloudflare mulai mengirim halaman 504.

Cara pertama yang biasanya diambil adalah Cache::remember. Hasilnya kelihatan memuaskan sampai cache-nya kedaluwarsa, lalu semua permintaan pada detik yang sama menemukan cache kosong dan bersama-sama menjalankan query berat itu. Halaman tetap lambat, hanya saja sekarang lambatnya terjadi serempak.

Cache::flexible dirancang untuk situasi itu.

Apa bedanya dengan Cache::remember

Dua-duanya menyimpan hasil query. Bedanya ada di apa yang terjadi ketika waktu simpan habis.

HalCache::rememberCache::flexible
Parameter waktusatu angkadua angka: segar dan basi
Saat waktu habissemua request menunggu callback selesainilai lama langsung dikembalikan
Perhitungan ulangdi depan, memblokir responsdi belakang, setelah respons dikirim
Risiko lonjakantinggi kalau callback lambatrendah, karena dijalankan satu proses
Data yang disajikanselalu hasil terbaruboleh beberapa detik atau menit lebih tua

Pemakaiannya cuma berubah sedikit:

PHPPHP
use Illuminate\Support\Facades\Cache;

$transmisi = Cache::flexible('filter.transmissions', [300, 600], function (): array {
    return CarListing::query()
        ->whereNotNull('transmission')
        ->where('transmission', '!=', '')
        ->distinct()
        ->pluck('transmission')
        ->sort()
        ->values()
        ->toArray();
});

Angka pertama, 300, adalah jendela segar dalam detik. Selama umur cache belum lewat lima menit, nilainya dianggap terbaru dan langsung dipakai.

Angka kedua, 600, adalah batas umur total. Antara menit kelima dan menit kesepuluh, nilai itu disebut basi tapi masih boleh disajikan. Pada rentang itulah flexible bekerja: nilai lama dikirim ke pengguna, sementara satu proses di belakang layar menghitung ulang.

Lewat menit kesepuluh, nilai benar-benar hilang dan permintaan berikutnya harus menunggu perhitungan seperti biasa.

Isi sebenarnya dari method ini

Kalau kamu penasaran, implementasinya di Laravel 13 cukup pendek dan layak dibaca. Yang dilakukan flexible kira-kira begini:

  1. Ambil dua kunci sekaligus: nilai cache, dan kunci pendamping illuminate:cache:flexible:created:<key> yang menyimpan waktu pembuatan.
  2. Kalau salah satunya kosong, jalankan callback sekarang, lalu simpan nilai dan waktunya dengan TTL sebesar angka kedua.
  3. Kalau created + angka pertama masih lebih besar dari sekarang, nilainya masih segar. Kembalikan langsung.
  4. Kalau sudah lewat, jadwalkan refresh lewat defer(), lalu kembalikan nilai lama tanpa menunggu.

Bagian keempat itu yang penting. Refresh-nya dibungkus defer(), artinya dijalankan setelah respons dikirim ke pengguna. Di dalamnya ada lock:

PHPPHP
$this->store->lock(
    "illuminate:cache:flexible:lock:{$this->itemKey($key)}",
    $lock['seconds'] ?? 0,
    $lock['owner'] ?? null,
)->get(function () use ($key, $callback, $created, $ttl) {
    if ($created !== $this->get(self::FLEXIBLE_CREATED_KEY_PREFIX.$key)) {
        return;
    }

    $this->putMany([
        $key => value($callback),
        self::FLEXIBLE_CREATED_KEY_PREFIX.$key => Carbon::now()->getTimestamp(),
    ], $ttl[1]);
});

Ada dua pengaman di situ. Lock memastikan hanya satu proses yang menghitung ulang, dan pengecekan created memastikan proses yang menang benar-benar masih memegang versi cache yang sama. Kalau ada proses lain yang sudah memperbarui sebelum lock didapat, pekerjaannya dibatalkan.

Hasil akhirnya: sepuluh pengguna yang membuka halaman pada detik yang sama semuanya mendapat respons cepat, dan hanya satu query berat berjalan di latar belakang.

Syarat yang mudah terlewat

Store harus mendukung lock. Baris $this->store->lock(...) akan gagal pada store yang tidak menyediakan lock, dan array store termasuk di situ. Redis, Memcached, database, dan file sama-sama mendukung lock, jadi untuk produksi pilihan ini aman. Untuk tes dengan CACHE_STORE=array, siapkan store lain atau uji di lingkungan yang memakai redis.

Refresh terjadi setelah respons. Itu kelebihan sekaligus keterbatasan. Kalau proses PHP dimatikan paksa tepat setelah respons dikirim, refresh bisa tidak pernah selesai. Di PHP-FPM dan Octane normal, defer() berjalan setelah respons selesai, jadi pekerjaannya tetap dieksekusi.

Parameter tambahan yang jarang dipakai. Signature lengkapnya menerima argumen keempat dan kelima:

PHPPHP
Cache::flexible(
    key: 'filter.transmissions',
    ttl: [300, 600],
    callback: fn () => hitungTransmisi(),
    lock: ['seconds' => 10],   // berapa lama menunggu lock
    alwaysDefer: true,         // selalu refresh di belakang, walau belum basi
);

alwaysDefer berguna kalau callback-nya cepat dan kamu ingin cache selalu diperbarui tanpa pernah memblokir.

Memilih dua angka itu

Ini bagian yang menentukan berhasil atau tidaknya, dan jawabannya bergantung pada satu pertanyaan: seberapa basi data ini boleh dilihat pengguna?

Jenis dataSegarBasiAlasan
Opsi filter, kategori, merek5 menit30 menitperubahannya jarang, dan salah satu merek muncul belakangan tidak merusak apa pun
Statistik dashboard1 menit10 menitangka yang telat semenit masih bisa dipakai untuk melihat tren
Daftar artikel atau produk30 detik5 menitperubahan terasa cepat, tapi tidak perlu real-time
Pencarian populer10 menit1 jamsifatnya agregat dan tidak sensitif
Saldo, stok, kuota, izin aksesjangan pakaijangan pakaikesalahan kecil di sini menjadi keluhan nyata

Baris terakhir penting. flexible sengaja menyajikan data yang mungkin sudah tidak benar, dan itu pertukaran yang buruk untuk angka uang atau hak akses.

Kapan sebaiknya tidak dipakai

  • Setelah pengguna menulis data. Kalau user baru menyimpan perubahan lalu halaman dimuat ulang, dia harus melihat perubahannya. Cache basi akan terasa seperti bug, bukan seperti performa.
  • Angka yang dijumlahkan atau diagregasi untuk keputusan. Total tagihan, sisa stok, jumlah kuota.
  • Data yang punya tenggat hukum atau audit. Laporan yang harus mencerminkan keadaan pada detik tertentu.
  • Query yang sudah cepat. Kalau query-nya 20 milidetik, menambah lapisan cache hanya menambah kompleksitas dan satu titik gagal.

Untuk kasus-kasus itu, Cache::remember dengan invalidasi eksplisit setelah penulisan masih lebih tepat, meski harus menerima risiko lonjakan saat kedaluwarsa.

Cache bukan pengganti index

Di kasus nyata yang saya baca, waktu muat turun dari sekitar 23,5 detik menjadi 5 detik setelah dua hal dikerjakan bersamaan: cache untuk query filter, dan penambahan index di kolom yang dipakai menyaring.

Kalau hanya cache yang dikerjakan, query pertama setelah cache basi tetap lambat, dan refresh di belakang layar tetap menghabiskan CPU. Kalau hanya index yang dikerjakan, lonjakan trafik masih bisa membuat database kewalahan.

GejalaPenyebabTindakan
Halaman lambat hanya sesekali, bersamaancache kedaluwarsa serempakflexible, atau lock saat mengisi cache
Lambat konsisten walau cache hangatquery memang beratindex, atau ubah bentuk query
Lambat saat filter tertentu dipakaikombinasi tidak terindexindex komposit sesuai pola filter
Database sibuk walau halaman cepatterlalu banyak kunci cache berbedakurangi kardinalitas kunci

Untuk soal index, saya pernah menulis lebih panjang di artikel trik SQL. Dua-duanya perlu, dan cache tidak akan menyelamatkan query yang memang salah bentuk.

Jebakan yang muncul setelah dipakai

Kunci cache yang meledak jumlahnya. Ini kesalahan paling umum. Kalau kunci dibuat dari seluruh kombinasi filter yang mungkin, filter.transmissions.brand=3.type=2.year=2024 dan seterusnya, hit rate-nya akan rendah dan cache-nya jadi beban, bukan bantuan.

Yang lebih baik: satu kunci per dimensi, bukan per kombinasi.

PHPPHP
foreach (['brands', 'transmissions', 'fuels', 'years'] as $dimensi) {
    $opsi[$dimensi] = Cache::flexible("filter.{$dimensi}", [300, 1800], fn () => ambilOpsi($dimensi));
}

Dengan begitu, empat kunci kecil melayani ratusan kombinasi filter.

Invalidasi setelah mengubah data. Cache::forget('filter.brands') tetap bekerja, tapi ingat bahwa nilai pendamping waktu pembuatan masih ada. Pada permintaan berikutnya, nilai utama kosong sehingga callback langsung dijalankan, jadi hasilnya benar. Yang perlu kamu pastikan hanya: lupakan kunci yang tepat, bukan kunci lain.

Deploy yang mengubah bentuk data. Kalau struktur hasil callback berubah, cache lama bisa membuat kode baru menerima bentuk lama. Biasakan menyertakan versi di nama kunci, misalnya v2.filter.brands, setiap kali bentuk datanya berubah.

Mengukur dengan cara yang salah. Membandingkan satu kali muat halaman tidak membuktikan apa pun. Ukur persentil ke-95 sebelum dan sesudah, hitung hit ratio, dan pastikan angka yang disajikan masih benar secara bisnis.

Uji juga skenario cache kosong: hapus cache, lalu tembak halaman itu dengan beberapa permintaan bersamaan. Kalau semuanya menjalankan callback sekaligus, lock-mu tidak bekerja seperti yang kamu kira.

Checklist sebelum memakai

  1. Data ini boleh basi berapa lama sebelum terasa salah?
  2. Store yang dipakai mendukung lock?
  3. Kunci cache-nya sedikit dan stabil, bukan hasil perkalian semua filter?
  4. Ada invalidasi setelah penulisan data?
  5. Query dasarnya sudah diindex?
  6. Sudah diukur p95 sebelum dan sesudah, bukan hanya perasaan lebih cepat?

Kalau nomor satu dijawab "tidak boleh basi sama sekali", berhenti di sini dan pakai remember dengan invalidasi.

Penutup

Cache::flexible bukan fitur yang mengubah arsitektur. Dia hanya memindahkan satu pekerjaan dari depan ke belakang layar, dan memberi izin pada data untuk terlihat sedikit lebih tua.

Tapi efeknya di halaman yang ramai cukup besar: tidak ada lagi semua orang menunggu query yang sama, dan tidak ada lagi lonjakan tepat saat cache kedaluwarsa. Untuk halaman filter, daftar kategori, dan statistik dashboard, itu kombinasi yang sulit ditolak.

Mulailah dari satu halaman yang paling lambat di aplikasimu. Ukur dulu, pilih dua angka yang masuk akal, lalu pasang. Kalau setelah itu halaman terasa lebih cepat tapi datanya terasa aneh, berarti angka pertamamu terlalu panjang.

Abd. Asis
Ditulis oleh
Abd. Asis

Software Developer dari Madura. Menulis tentang PHP, Laravel, dan pengembangan web modern dalam Bahasa Indonesia.

Artikel Terkait

Tulisan lain yang mungkin menarik buatmu.

Kiat Menguji Keamanan Payment Gateway Sebelum Uang Sungguhan Lewat
securitypayment-gateway

Kiat Menguji Keamanan Payment Gateway Sebelum Uang Sungguhan Lewat

Dua belas area yang wajib diuji, dari manipulasi nominal dan webhook palsu sampai race condition dan isolasi data merchant, lengkap dengan skenario serangan dan cara membuktikannya.

Abd. Asis
Sepuluh Trik SQL yang Memisahkan Backend Engineer dari Penulis Query
sqlpostgresql

Sepuluh Trik SQL yang Memisahkan Backend Engineer dari Penulis Query

Dari SELECT * sampai keyset pagination. Trik yang sama-sama diketahui orang, tapi jarang dipakai saat query sudah mulai lambat di produksi.

Abd. Asis
Cara Mendesain Aplikasi Ujian yang Tidak Bisa Ditebak Siswa
laravelcbt

Cara Mendesain Aplikasi Ujian yang Tidak Bisa Ditebak Siswa

Pelajaran dari modul CBT Buku Sekolah: kenapa soal harus di-snapshot, waktu harus milik server, pengacakan harus deterministik, dan token tidak boleh bisa dipakai dua kali.

Abd. Asis
Merge PDF di Laravel dengan FPDI
laravelphp

Merge PDF di Laravel dengan FPDI

Cara menggabungkan beberapa file PDF menjadi satu di Laravel menggunakan spatie/laravel-pdf untuk generate dan setasign/fpdi untuk merge, lengkap tiga studi kasus nyata.

Abd. Asis
Laravel Policies: Otorisasi Berbasis Model
laravelphp

Laravel Policies: Otorisasi Berbasis Model

Pelajari cara menggunakan Laravel Policies untuk otorisasi berbasis model — dari setup awal, Response object, before() hook, hingga pola multi-tenant SaaS yang siap produksi.

Abd. Asis
Livewire, Vue, atau React untuk Laravel?
laravelphp

Livewire, Vue, atau React untuk Laravel?

Perbandingan praktis Livewire, Vue.js, dan React untuk Laravel — kapan masing-masing pilihan masuk akal, dan bagaimana memilih tanpa terjebak debat framework.

Abd. Asis