Cache
Cache mudah dipasang dan sulit dibersihkan. Materi ini tentang memilih driver yang benar, memberi nama kunci yang bisa dihapus, dan menghindari dua jebakan klasik: stampede dan data basi.
Intisari
Cache::remember($kunci, $ttl, $closure)menutup 90% kebutuhan.- Driver
filedanarraytidak dibagi antar kontainer โ di AWS berarti Redis. - Rancang kunci-nya sejak awal; kunci yang tidak berpola tidak bisa dihapus secara selektif.
- TTL pendek untuk data yang berubah; invalidasi eksplisit untuk data yang harus benar.
- Cache stampede: saat entri populer kedaluwarsa, semua permintaan menghitung ulang bersamaan. Obatnya
Cache::lock()atauflexible().
Pola yang dipakai sehari-hari
$kategori = Cache::remember('kategori:aktif', now()->addHour(), function () {
return Kategori::where('aktif', true)->orderBy('urutan')->get();
});
Cache::rememberForever('pengaturan', fn () => Pengaturan::pluck('nilai', 'kunci'));
Cache::put('kunci', $nilai, now()->addMinutes(10));
Cache::get('kunci', 'nilai default');
Cache::forget('kunci');
Cache::flush(); // hampir tidak pernah benar di produksi
Cache::increment('kunjungan:'.date('Y-m-d'));
// Laravel 13: perpanjang TTL tanpa membaca dan menulis ulang nilainya
Cache::touch('sesi:'.$id, now()->addMinutes(30));
Memilih driver
| Driver | Dibagi antar proses? | Kapan |
|---|---|---|
array | Tidak โ hilang di akhir permintaan | Tes |
file | Hanya dalam satu mesin | Pengembangan lokal |
database | Ya | Skala kecil; menambah beban ke database yang justru ingin kamu ringankan |
redis | Ya | Pilihan produksi. Mendukung lock dan tag |
dynamodb | Ya | Serverless penuh, tanpa mengelola node |
octane | Per proses worker | Sangat cepat, tapi tiap worker punya salinannya sendiri (Fase 8) |
Ini penyebab bug "kadang benar, kadang basi". Dengan driver file dan tiga kontainer,
Cache::forget() hanya menghapus salinan di kontainer yang kebetulan menangani permintaan itu.
Dua kontainer lain tetap menyajikan data lama sampai TTL-nya habis โ dan gejalanya berubah-ubah tergantung ke
mana load balancer mengirim permintaan. Begitu ada lebih dari satu proses aplikasi, cache harus
bersama.
Rancang kuncinya, bukan cuma nilainya
// Buruk โ tidak bisa dihapus secara selektif
Cache::remember('data', 3600, ...);
// Baik โ berpola, mengandung semua yang membedakan hasilnya
Cache::remember("produk:{$produk->id}:v2", 3600, ...);
Cache::remember("katalog:kategori:{$k->id}:halaman:{$halaman}", 600, ...);
Cache::remember("dasbor:tenant:{$tenantId}:harian:".today()->toDateString(), 3600, ...);
Dua hal yang wajib masuk ke kunci. Pertama, segala sesuatu yang mengubah hasilnya โ
termasuk ID tenant dan ID pengguna. Kunci yang lupa menyertakan tenant adalah cara paling langsung menyajikan
data satu pelanggan kepada pelanggan lain. Kedua, sebuah penanda versi: saat bentuk datanya
berubah, naikkan v2 jadi v3 dan seluruh entri lama menjadi tidak relevan tanpa perlu
dihapus satu per satu.
Invalidasi
// 1. TTL pendek โ paling sederhana, cocok untuk data yang tidak harus persis
Cache::remember('statistik', now()->addMinutes(5), ...);
// 2. Eksplisit saat data berubah โ untuk data yang harus benar
class ProdukObserver
{
public function saved(Produk $p): void { $this->bersihkan($p); }
public function deleted(Produk $p): void { $this->bersihkan($p); }
private function bersihkan(Produk $p): void
{
Cache::forget("produk:{$p->id}:v2");
Cache::forget("katalog:kategori:{$p->kategori_id}:halaman:1");
}
}
// 3. Tag โ hapus sekelompok sekaligus (Redis dan Memcached saja)
Cache::tags(['produk', "kategori:{$id}"])->remember('daftar', 3600, ...);
Cache::tags(["kategori:{$id}"])->flush();
Cache stampede
Sebuah entri populer kedaluwarsa pada pukul 09:00:00. Dalam detik itu, dua ratus permintaan menemukan cache kosong, dan dua ratus di antaranya menjalankan query mahal yang sama secara bersamaan. Database yang tadinya santai langsung tersedak.
// Cara 1 โ kunci: hanya satu yang menghitung, sisanya menunggu hasilnya
$nilai = Cache::lock('hitung:laporan', 10)->block(5, function () {
return Cache::remember('laporan', 3600, fn () => $this->hitungBerat());
});
// Cara 2 โ nilai basi disajikan dulu, penyegarannya dilakukan di latar
$nilai = Cache::flexible('laporan', [300, 3600], fn () => $this->hitungBerat());
flexible() menerima dua angka: selama 300 detik nilainya dianggap segar; antara 300 dan 3600 detik
nilainya tetap disajikan sambil diperbarui di latar belakang. Tidak ada satu pun pengguna yang menunggu
perhitungan ulang.
Apa yang layak di-cache
| Kandidat | Layak? | Catatan |
|---|---|---|
| Data referensi (kategori, wilayah) | Sangat | Jarang berubah, sering dibaca |
| Hasil agregasi mahal | Sangat | Pakai flexible() |
| Respons API pihak ketiga | Ya | Sekaligus melindungi dari rate limit mereka |
| Detail satu produk | Kadang | Ukur dulu โ sering kali index sudah cukup |
| Data khusus pengguna | Hati-hati | ID pengguna wajib ada di kunci |
| Saldo, stok, harga aktif | Tidak | Basi beberapa detik saja sudah merugikan |
Cache bukan pengganti index. Query 800 ms yang di-cache tetaplah query 800 ms โ kamu cuma memindahkan penderitaannya ke pengguna yang apes mendapat cache miss. Perbaiki query-nya dulu (Fase 7), baru pertimbangkan cache.
Latihan: cache daftar kategori dengan kunci berpola dan TTL satu jam, lalu tambahkan observer yang
menghapusnya saat kategori berubah. Setelah itu ganti CACHE_STORE jadi file,
jalankan dua proses php artisan serve di port berbeda, dan buktikan sendiri bahwa
forget() di satu proses tidak berpengaruh pada yang lain.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.