Optimasi query dan index membuat pekerjaan database seefisien mungkin — tetapi pekerjaan terbaik adalah yang tidak dikerjakan ulang. Laporan agregat, panggilan API eksternal, dan potongan HTML yang mahal dibangun sering kali menghasilkan data yang sama persis untuk ribuan permintaan berikutnya. Application caching menyimpan hasil komputasi itu dan menyajikannya dalam sepersekian milidetik. Operasi dasar cache (put, get, forget, driver, dan TTL) sudah dibahas di chapter Cache Operations ; di sini kita fokus pada strategi performa: apa yang layak di-cache, bagaimana mencegah stampede, dan kapan cache justru menambah masalah.

Taksonomi Kandidat Cache #

Tidak semua komputasi layak di-cache. Tabel ini memetakan kandidat berdasarkan sifatnya:

KandidatContohKarakteristik
Agregat databasejumlah post per kategori, total viewsQuery GROUP BY di tabel besar
Panggilan API eksternalkurs valuta, data cuacaLatensi jaringan + rate limit
Fragment rendersidebar, menu beratHTML yang sama tiap pengunjung
Nilai konfigurasi berathasil parsing file besarDihitung sekali, jarang berubah

Penanda umumnya: mahal dihitung, sering diminta, dan tidak berubah tiap detik. Data yang personal per-pengguna (halaman profil) biasanya buruk untuk cache karena rasio hit-nya rendah.

Cache::remember(): Alat Utama #

Method andalan caching strategis adalah Cache::remember(key, ttl, closure) — cek cache, bila kosong jalankan closure lalu simpan hasilnya. Demo berikut membangun laporan agregat per kategori dari 10.000 post (total views + jumlah kata dari isi post):

php
use App\Models\Category;
use App\Models\Post;
use Illuminate\Support\Facades\Cache;

function laporanBerat(): array
{
    $laporan = [];
    foreach (Category::orderBy('id')->get() as $kategori) {
        $posts = Post::where('category_id', $kategori->id)->get(['views', 'isi']);
        $kata = 0;
        foreach ($posts as $post) {
            $kata += str_word_count(substr($post->isi, 0, 2000));
        }
        $laporan[$kategori->nama] = [
            'post' => $posts->count(),
            'views' => $posts->sum('views'),
            'kata' => $kata,
        ];
    }
    return $laporan;
}

$mulai = hrtime(true);
$laporan = Cache::remember('laporan.kategori', 600, fn () => laporanBerat());
$ms1 = (hrtime(true) - $mulai) / 1e6;

$mulai = hrtime(true);
$laporan = Cache::remember('laporan.kategori', 600, fn () => laporanBerat());
$ms2 = (hrtime(true) - $mulai) / 1e6;

echo sprintf("remember (membangun): %6.1f ms\n", $ms1);
echo sprintf("remember (cache hit): %6.2f ms\n", $ms2);

Output yang Dihasilkan:

text
remember (membangun):  215.7 ms
remember (cache hit):  0.24 ms

Panggilan pertama membayar harga penuh 215,7 ms dan menyimpan hasilnya selama 600 detik. Panggilan kedua menjawab dalam 0,24 ms — sekitar 900 kali lebih cepat, karena hasilnya dibaca dari tabel cache (driver database di lingkungan kita) tanpa menyentuh logika laporan sama sekali. Dengan laporan ini diminta 1.000 kali per hari, penghematannya berarti puluhan menit waktu komputasi.

Stampede: Saat Cache Kedaluwarsa Bersamaan #

Bayangkan laporan tadi kedaluwarsa tepat saat 100 pengunjung memuat halaman yang sama dalam satu detik. Tanpa perlindungan, 100 proses sekaligus menjalankan laporanBerat() — peristiwa ini disebut cache stampede, dan puncak beban 100 × 215 ms bisa menjatuhkan server yang sehat. Solusi Laravel: atomic lock — hanya satu proses yang boleh membangun ulang:

php
use Illuminate\Support\Facades\Cache;

Cache::forget('laporan.lock');

$lockA = Cache::lock('laporan.lock', 10);
echo $lockA->get() ? "proses A: kunci DIPEGANG\n" : "proses A: gagal\n";

$lockB = Cache::lock('laporan.lock', 10);
echo $lockB->get() ? "proses B: kunci DIPEGANG\n" : "proses B: kunci dipakai proses lain\n";

$lockA->release();
echo $lockB->get() ? "proses B: kunci DIPEGANG (setelah A melepas)\n" : "proses B: gagal\n";
$lockB->release();

Output yang Dihasilkan:

text
proses A: kunci DIPEGANG
proses B: kunci dipakai proses lain
proses B: kunci DIPEGANG (setelah A melepas)

Saat cache kosong, proses pertama yang berhasil mengunci membangun ulang; proses lain gagal mengunci dan menunggu/menyajikan nilai lama — tidak ada pasukan yang menyerbu database bersamaan. Pola lengkapnya: cek cache → bila kosong, Cache::lock()->block($detik, fn () => Cache::remember(...)).

Strategi TTL per Volatilitas #

TTL bukan angka untung-untungan — ia menegaskan kapan data boleh basi:

Jenis dataTTL wajarAlasan
Kurs/berita eksternal5–15 menitSumber berubah berkala
Agregat dashboard1–24 jamTren, bukan angka real-time
Fragment navigasi24 jam + invalidasi eventBerubah hanya saat konten berubah
Hasil perhitungan murnirememberForever + invalidasiTidak pernah berubah sendiri

Semakin pendek TTL, semakin segar data dan semakin sering pembangunan ulang terjadi. Semakin panjang, semakin murah — dan semakin besar ketergantungan pada disiplin invalidasi.

Disiplin Invalidasi #

Cache yang salah isi lebih buruk daripada tidak ada cache: pengguna melihat angka bohong dengan percaya diri. Aturannya: setiap kode yang mengubah sumber data wajib memikirkan kunci cache yang terdampak. Verifikasi mekanismenya murah:

php
use Illuminate\Support\Facades\Cache;

Cache::forget('jumlah.post');
Cache::remember('jumlah.post', 3600, fn () => 10000);
echo 'sebelum invalidasi: ', Cache::get('jumlah.post'), "\n";

Cache::forget('jumlah.post');
echo 'setelah forget: ', json_encode(Cache::get('jumlah.post')), "\n";
echo 'ada kunci? ', Cache::has('jumlah.post') ? 'masih ada' : 'hilang', "\n";

Output yang Dihasilkan:

text
sebelum invalidasi: 10000
setelah forget: null
ada kunci? hilang

forget() menghapus kunci sehingga remember() berikutnya terpaksa membangun ulang. Pola yang sehat: panggil Cache::forget('jumlah.post') di observer model Post setiap kali post dibuat/dihapus, bukan menunggu TTL habis. Strategi penamaan kunci, tags, dan event-driven invalidasi dibahas tuntas di chapter Cache Invalidation .

Kejujuran Metrik Cache #

Cache bukan pereda ajaib untuk semua kelambatan. Dua pertanyaan wajib sebelum menambah cache: berapa rasio hit yang realistis, dan apakah yang di-cache memang mahal? Widget yang diminta 10 kali sehari dengan komputasi 2 ms tidak butuh cache — kompleksitas invalidasinya lebih mahal daripada penghematannya. Ukur dulu (chapter Profiling Laravel ), lalu cache titik yang terbukti mahal dan sering diminta.

Cache meringankan pekerjaan yang berulang pada level aplikasi. Bab selanjutnya menuruni tumpukan ke infrastruktur framework itu sendiri: membekukan definisi route agar setiap permintaan tidak mengurai ulang berkas routing.