Cache adalah salinan data yang disimpan di tempat yang lebih cepat dijangkau, supaya sistem gak perlu ke sumber aslinya (biasanya database) setiap kali. Caching strategy adalah aturan kapan salinan itu diisi, diperbarui, dan dibuang. Bayangin papan harga di warung kopi. Harga ditulis di papan supaya kasir gak perlu membuka buku stok tiap ada yang bertanya. Tapi kalau harga naik dan papannya lupa dihapus, pembeli melihat harga lama.
🧪 Tebakan cepat: halaman Konser Nusantara disimpan di Redis dengan TTL 30 detik. Di detik 20, harganya turun dari Rp750.000 jadi Rp700.000: database diubah, cache dibiarkan. Sampai kapan pengguna masih melihat Rp750.000: langsung berubah, sampai sekitar detik 30, atau selamanya? Tebak dulu, jawabannya ada di bagian "Invalidation: saat data berubah".
Hit, miss, dan hit ratio
TiketNada menyimpan data konser di Redis (database di memori yang sangat cepat) dengan key event:42. Saat request datang, data yang ditemukan di cache disebut cache hit. Kalau gak ada, itu cache miss, dan data harus diambil dari PostgreSQL. Hit ratio = hit ÷ total request.
Model contoh di Lab: 50 request baca per detik (satu tiap 20 ms) selama 60 detik, jadi 3.000 request. Saat war tiket, satu query database butuh 200 ms. Di detik 20 harga turun. Cache bisa ada di banyak tempat (browser, CDN, server aplikasi, database); lesson ini membahas cache aplikasi di Redis.
Cache-aside
Strategi paling umum: aplikasi cek cache dulu; kalau miss, ambil dari database lalu simpan ke cache. Disebut juga lazy loading, karena data baru masuk cache saat ada yang meminta.
// Baca: cache-aside dengan TTL 30 detik (ioredis + node-postgres)
async function getEvent(id) {
const key = "event:" + id;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached); // hit
const { rows } = await pool.query("SELECT * FROM events WHERE id = $1", [id]); // miss: 200 ms
await redis.set(key, JSON.stringify(rows[0]), "EX", 30); // simpan, hapus otomatis setelah 30 detik
return rows[0];
}
redis.get(key)mengembalikan string yang tersimpan, ataunullkalau gak ada.- Kalau ada, langsung dikembalikan. Database gak disentuh.
- Kalau
null, query database, laluredis.set(key, nilai, "EX", 30)menyimpannya dengan TTL (time to live) 30 detik: setelah itu Redis menghapusnya sendiri.
Hasilnya di model: dari 3.000 request, cuma 20 yang miss. Hit ratio 99,3%.
TTL dan cache stampede
Dari mana 20 miss itu? Di detik 0 cache masih kosong. Selama 200 ms query pertama berjalan, 10 request lain datang, semuanya miss, dan semuanya query database sendiri-sendiri. Hal yang sama terjadi lagi saat key kedaluwarsa. Ini cache stampede (disebut juga thundering herd): key hilang, lalu banyak request menghantam database bersamaan. Di war tiket, 10 bisa jadi 10.000, dan database tumbang tepat saat paling dibutuhkan. Efek yang sama muncul kalau banyak key dibuat dengan TTL sama sehingga kedaluwarsa serentak.
Obatnya:
- Lock (single-flight): request pertama yang miss mengambil kunci, misalnya
SET lock:event:42 1 NX PX 5000, dan cuma dia yang query. Sisanya menunggu sebentar lalu membaca cache lagi. Di model, 20 query database turun jadi 2. - Jitter: tambahkan beberapa detik acak ke TTL supaya key gak kedaluwarsa bersamaan.
- Pre-warming: isi cache sebelum war tiket dimulai.
TTL juga trade-off. TTL 5 detik: hit ratio turun ke 96,0% dan database menerima 120 query. TTL 5 menit: hit ratio 99,7%, tapi data lama bertahan lama, seperti di bagian berikut.
Invalidation: saat data berubah
Di detik 20, harga diubah di database, tapi event:42 di Redis masih berisi Rp750.000. Pengguna terus melihat harga lama sampai key itu kedaluwarsa. Di model, 10 query dari stampede awal masing-masing menulis key lagi dengan TTL baru, yang terakhir di detik 0,38, jadi key kedaluwarsa di detik 30,38. Jawaban tebakannya: sampai sekitar detik 30. Itu 519 request yang membaca harga lama, disebut stale read. Dengan TTL 5 menit, harga lama bertahan sampai akhir: 2.000 stale read.
Invalidation artinya membuang salinan saat aslinya berubah:
// Tulis: ubah database, lalu hapus key-nya
async function updateHarga(id, harga) {
await pool.query("UPDATE events SET harga = $1 WHERE id = $2", [harga, id]);
await redis.del("event:" + id); // request berikutnya miss dan membaca harga baru
}
Hasilnya 0 stale read, dengan harga satu burst miss tambahan di detik 20.
Write-through dan write-back
Dua strategi lain mengurus cache saat menulis:
| Strategi | Saat harga diubah | Kelebihan | Risiko |
|---|---|---|---|
| Cache-aside | tulis DB, hapus key (atau tunggu TTL) | cuma data yang diminta yang masuk cache | miss pertama lambat, stale kalau lupa menghapus |
| Write-through | tulis DB dan cache sekaligus | cache selalu segar, 0 stale read | tiap write dua kali kerja; cache bisa berisi data yang jarang dibaca |
| Write-back (write-behind) | tulis cache dulu, DB menyusul (di model, 5 detik kemudian) | write sangat cepat | data hilang kalau cache mati sebelum sempat ditulis ke DB |
Di model write-back, Redis restart di detik 22, sebelum flush di detik 25. Perubahan harganya hilang: database tetap Rp750.000, dan 1.900 request sesudahnya membaca harga yang salah. Jadi write-back cocok untuk data yang boleh hilang sedikit (hitungan view, misalnya), bukan untuk harga atau uang.
Kapan cache-aside lebih tepat daripada write-through? Saat data jarang berubah dan gak semua data perlu di-cache: cuma konser yang sedang dibuka orang yang masuk Redis.
📌 Cache di browser: Cache-Control dan ETag
Browser juga menyimpan cache, diatur header dari server (detailnya di lesson API Caching):
Cache-Control: max-age=31536000, immutableartinya simpan selama 1 tahun (31.536.000 detik) dan gak perlu revalidasi sama sekali. Cocok untuk file yang namanya berisi hash isinya, misalnyaapp.3f9a1c.js: kalau isinya berubah, namanya ikut berubah.- ETag adalah tanda versi isi file, biasanya hash dari kontennya, misalnya
"3f9a1c". Last-Modified adalah timestamp terakhir file diubah. Saat cache kedaluwarsa, browser bertanya denganIf-None-Match(ETag) atauIf-Modified-Since(timestamp), dan server menjawab304 Not Modifiedkalau gak berubah. ETag lebih akurat: timestamp cuma sampai detik, dan bisa berubah walau isinya sama.
⚠️ Jebakan yang sering ketemu
- Lupa menghapus key saat data berubah. Pengguna melihat harga lama sampai TTL habis. Hapus juga cache turunan, misalnya daftar konser.
- Tanpa perlindungan stampede. Key populer kedaluwarsa, dan semua request menghantam database bersamaan. Pakai lock atau jitter.
- Write-back untuk data penting. Cache mati sebelum flush, data hilang. Untuk harga, stok, dan uang, pakai cache-aside atau write-through.
- TTL asal panjang. Hit ratio naik sedikit, tapi data basi bertahan lama. Pilih TTL dari seberapa lama data boleh telat.
TL;DR: Cache menyimpan salinan data di tempat cepat (Redis) supaya database gak dipanggil tiap kali; hit ratio = hit ÷ total. Cache-aside: cek cache, kalau miss baca database lalu simpan dengan TTL; paling umum, cocok saat data jarang berubah dan gak semua perlu di-cache. Saat key hilang, banyak request bisa menghantam database bersamaan: cache stampede (thundering herd); obatnya lock, jitter, pre-warming. Kalau data berubah, hapus key-nya (invalidation), kalau gak, pengguna membaca data basi sampai TTL habis. Write-through menulis cache dan database sekaligus (konsisten, write lebih lambat); write-back menulis cache dulu dan database menyusul (cepat, tapi bisa hilang). Di browser, max-age=31536000, immutable = simpan setahun tanpa revalidasi; ETag = tanda versi dari isi, Last-Modified = timestamp perubahan.