Cache adalah salinan data yang disimpan dekat dengan pemakainya agar permintaan berikutnya tidak perlu menempuh seluruh jalur pengambilan. Topik ini membahas konsep cache hit dan cache miss, empat lapisan cache dari browser sampai basis data, karakteristik tiap lapisan, serta pengukuran efektivitasnya melalui simulasi Python dan demo interaktif yang dapat dicoba langsung.
Cache hit, cache miss, dan rasio hit#
Ketika permintaan (request) tiba, cache memeriksa apakah salinan data yang diminta ada dan masih berlaku. Keadaan salinan tersedia dan berlaku disebut cache hit, yaitu permintaan dijawab dari cache tanpa menyentuh sumbernya. Keadaan sebaliknya disebut cache miss, yaitu permintaan harus melanjutkan perjalanan ke sumber data, dan salinan yang diperoleh biasanya disimpan ke cache agar permintaan berikutnya dijawab lebih cepat.
Efektivitas sebuah cache diukur dengan cache hit ratio, yaitu porsi permintaan yang dijawab langsung oleh cache:
Apabila terdapat beberapa lapisan cache, peluang permintaan mencapai sumber adalah hasil kali peluang miss tiap lapisan. Dua lapisan yang masing-masing mencapai rasio hit 80% menyisakan hanya , sehingga 4% permintaan yang sampai ke sumber data. Kerja berlapis inilah yang membuat caching menjadi salah satu alat paling berpengaruh pada latensi dan beban sumber data.
Empat lapisan cache#
Sebuah permintaan web biasanya melewati empat lapisan cache sebelum sampai ke data aslinya. Perjalanan dimulai di browser pengguna yang menyimpan aset statis, berlanjut ke CDN yang menyajikan konten populer dari lokasi dekat pengguna, kemudian ke cache aplikasi yang menyimpan hasil kueri, dan hanya setelah semua lapisan meneruskan permintaan, basis data mengerjakannya. Lapisan pertama yang masih menyimpan salinan yang valid menghentikan perjalanan permintaan di tempatnya.
Empat lapisan itu dimiliki dan dikelola pihak yang berbeda. Cache browser adalah milik tiap pengguna dan disebut cache pribadi karena isinya tidak dibagikan. Cache CDN dan cache aplikasi bersifat dibagikan (shared), yaitu satu salinan melayani banyak pengguna sekaligus, sehingga rasio hitnya jauh lebih tinggi tetapi isinya harus aman untuk semua pihak. Cache basis data berada di dalam sistem basis data itu sendiri, berupa penyangga halaman data dan indeks yang dikelola secara otomatis tanpa kode tambahan.
Karakteristik tiap lapisan#
| Lapisan | Penyimpannya | Isi yang khas | Kepemilikan data |
|---|---|---|---|
| Cache browser | browser pengguna | aset statis, respons HTTP | pribadi per pengguna |
| Cache CDN | penyedia CDN | konten populer, berkas besar | dibagikan banyak pengguna |
| Cache aplikasi | aplikasi | hasil kueri, objek data | dibagikan antarinstance |
| Cache basis data | sistem basis data | halaman data, indeks | internal, dikelola otomatis |
Tabel tersebut menegaskan dua pembeda penting. Pembeda pertama adalah kepemilikan: cache pribadi hanya melihat data satu pengguna, sedangkan cache bersama menyatukan permintaan banyak pengguna sehingga salinan yang sama dihitung sekali untuk semua. Pembeda kedua adalah kendali: lapisan browser dan CDN tunduk pada header HTTP yang dikirim aplikasi, lapisan aplikasi dikelola penuh oleh kode, dan lapisan basis data hampir sepenuhnya otomatis. Karena kendalinya berbeda, cara mengukur dan memperbaiki tiap lapisan juga berbeda.
Mengukur cache hit ratio dengan simulasi#
Simulasi berikut menjalankan sebuah cache aplikasi berkapasitas empat entri dengan penggusuran LRU (least recently used), yaitu entri yang paling lama tidak diakses, dibuang terlebih dahulu ketika cache penuh. Urutan aksesnya meniru pola nyata: sebagian kunci diakses berulang-ulang, sedangkan sebagian lain hanya lewat sekali.
# Simulasi cache hit dan cache miss pada cache aplikasi berkapasitas empat
# entri dengan penggusuran LRU (least recently used), yaitu entri yang paling
# lama tidak diakses yang dibuang terlebih dahulu.
KAPASITAS = 4
URUTAN_AKSES = [
"profil-ali", "profil-ali", "profil-budi", "profil-citra",
"profil-dewi", "profil-ali", "profil-budi", "profil-edi",
"profil-citra", "profil-ali", "profil-dewi", "profil-farah",
"profil-ali", "profil-dewi", "profil-gita", "profil-ali",
"profil-dewi", "profil-hana", "profil-ali", "profil-dewi",
]
cache = [] # daftar kunci, dari yang paling lama tidak diakses
hit = 0
miss = 0
for i, kunci in enumerate(URUTAN_AKSES, 1):
if kunci in cache:
hit += 1
cache.remove(kunci)
cache.append(kunci) # segarkan posisi: kunci ini jadi paling baru
print(f"akses {i:>2}: {kunci:<13} CACHE HIT")
else:
miss += 1
korban = ""
if len(cache) >= KAPASITAS:
korban = f", menggusur {cache[0]}"
cache.pop(0)
cache.append(kunci)
print(f"akses {i:>2}: {kunci:<13} CACHE MISS (dari basis data{korban})")
print()
print(f"cache hit : {hit}")
print(f"cache miss: {miss}")
print(f"cache hit ratio: {hit / (hit + miss) * 100:.1f}%")akses 1: profil-ali CACHE MISS (dari basis data)
akses 2: profil-ali CACHE HIT
akses 3: profil-budi CACHE MISS (dari basis data)
akses 4: profil-citra CACHE MISS (dari basis data)
akses 5: profil-dewi CACHE MISS (dari basis data)
akses 6: profil-ali CACHE HIT
akses 7: profil-budi CACHE HIT
akses 8: profil-edi CACHE MISS (dari basis data, menggusur profil-citra)
akses 9: profil-citra CACHE MISS (dari basis data, menggusur profil-dewi)
akses 10: profil-ali CACHE HIT
akses 11: profil-dewi CACHE MISS (dari basis data, menggusur profil-budi)
akses 12: profil-farah CACHE MISS (dari basis data, menggusur profil-edi)
akses 13: profil-ali CACHE HIT
akses 14: profil-dewi CACHE HIT
akses 15: profil-gita CACHE MISS (dari basis data, menggusur profil-citra)
akses 16: profil-ali CACHE HIT
akses 17: profil-dewi CACHE HIT
akses 18: profil-hana CACHE MISS (dari basis data, menggusur profil-farah)
akses 19: profil-ali CACHE HIT
akses 20: profil-dewi CACHE HIT
cache hit : 10
cache miss: 10
cache hit ratio: 50.0%Dua hal terbaca langsung dari keluaran tersebut. Pertama, kunci yang diakses berulang seperti profil-ali dan profil-dewi tetap panas di cache, sehingga setengah dari seluruh akses dijawab tanpa menyentuh basis data. Kedua, penggusuran LRU selalu membuang entri yang paling lama tidak disentuh, bukan entri yang paling lama berada di cache, sehingga kunci yang sedang populer tidak terbuang meskipun sudah lama dimasukkan.
Mencoba demo interaktif#
Demo berikut mensimulasikan cache yang sama dengan urutan akses yang ditentukan pembaca. Cache demo berkapasitas empat entri dan memakai penggusuran LRU seperti pada simulasi sebelumnya, sehingga perilaku keduanya dapat dibandingkan langsung.
Jalankan demo, lalu lakukan tiga percobaan berikut secara berurutan.
- Klik tombol profil-ali dua kali dan amati bahwa akses kedua menghasilkan cache hit, kemudian lihat angka cache hit ratio yang naik.
- Klik empat kunci berbeda hingga cache penuh, lalu klik kunci kelima dan amati entri mana yang digusur dari slot cache.
- Klik kembali kunci yang baru saja digusur dan amati bahwa ia kembali sebagai cache miss, karena salinannya memang telah dibuang.
Melalui ketiga percobaan itu, hubungan antara urutan akses, penggusuran, dan rasio hit terlihat tanpa perlu membaca kodenya terlebih dahulu. Kode demo yang ditampilkan di sampingnya memperlihatkan bahwa logika simulatornya hanya berupa pemeriksaan keanggotaan, pembaruan posisi, dan pembuangan entri.
Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simulator cache hit dan cache miss</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<h1>Simulator cache hit dan cache miss</h1>
<p>Klik salah satu kunci untuk mensimulasikan akses aplikasi ke cache.
Cache berkapasitas empat entri dan menggunakan penggusuran LRU,
yaitu entri yang paling lama tidak diakses dibuang terlebih dahulu.</p>
<div id="tombol-kunci"></div>
<h2>Isi cache</h2>
<div id="isi-cache"></div>
<h2>Statistik</h2>
<p id="ringkasan">Cache hit: 0 · Cache miss: 0 · Cache hit ratio: -</p>
<h2>Riwayat akses</h2>
<ul id="log">
<li class="kosong">Belum ada akses. Klik salah satu kunci di atas.</li>
</ul>
<script src="index.js"></script>
<style>
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 12px 16px;
color: #1f2d3d;
}
h1 { font-size: 18px; margin: 0 0 4px; }
h2 { font-size: 14px; margin: 14px 0 6px; }
p { font-size: 13px; margin: 6px 0; }
#tombol-kunci {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#tombol-kunci button {
font-size: 13px;
padding: 6px 10px;
border: 1px solid #5b7a9d;
border-radius: 6px;
background: #eef3f8;
cursor: pointer;
}
#tombol-kunci button:hover { background: #dbe7f3; }
#isi-cache {
display: flex;
gap: 6px;
}
.slot {
width: 110px;
min-height: 34px;
border: 1px dashed #8a94a0;
border-radius: 6px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #8a94a0;
}
.slot.terisi {
border-style: solid;
border-color: #5b7a9d;
background: #eef3f8;
color: #1f2d3d;
}
.slot.terbaru { background: #dbe7f3; }
#ringkasan { font-weight: bold; }
#log {
font-size: 12px;
margin: 0;
padding-left: 18px;
max-height: 120px;
overflow-y: auto;
}
#log li { margin: 2px 0; }
.badge {
display: inline-block;
width: 84px;
text-align: center;
border-radius: 4px;
font-weight: bold;
}
.badge.hit { background: #e6f4ea; color: #1b7a3d; }
.badge.miss { background: #fdecea; color: #a12622; }
.kosong { color: #8a94a0; }
</style>
<style>
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 12px 16px;
color: #1f2d3d;
}
h1 { font-size: 18px; margin: 0 0 4px; }
h2 { font-size: 14px; margin: 14px 0 6px; }
p { font-size: 13px; margin: 6px 0; }
#tombol-kunci {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#tombol-kunci button {
font-size: 13px;
padding: 6px 10px;
border: 1px solid #5b7a9d;
border-radius: 6px;
background: #eef3f8;
cursor: pointer;
}
#tombol-kunci button:hover { background: #dbe7f3; }
#isi-cache {
display: flex;
gap: 6px;
}
.slot {
width: 110px;
min-height: 34px;
border: 1px dashed #8a94a0;
border-radius: 6px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #8a94a0;
}
.slot.terisi {
border-style: solid;
border-color: #5b7a9d;
background: #eef3f8;
color: #1f2d3d;
}
.slot.terbaru { background: #dbe7f3; }
#ringkasan { font-weight: bold; }
#log {
font-size: 12px;
margin: 0;
padding-left: 18px;
max-height: 120px;
overflow-y: auto;
}
#log li { margin: 2px 0; }
.badge {
display: inline-block;
width: 84px;
text-align: center;
border-radius: 4px;
font-weight: bold;
}
.badge.hit { background: #e6f4ea; color: #1b7a3d; }
.badge.miss { background: #fdecea; color: #a12622; }
.kosong { color: #8a94a0; }
</style>
<script>
// Simulator cache hit dan cache miss dengan penggusuran LRU.
// State disimpan pada tiga variabel sederhana: isi cache, hitungan hit, dan miss.
const KUNCI = [
"profil-ali", "profil-budi", "profil-citra", "profil-dewi",
"profil-edi", "profil-farah", "profil-gita", "profil-hana",
];
const KAPASITAS = 4;
const cache = []; // daftar kunci, dari yang paling lama tidak diakses
let hit = 0;
let miss = 0;
const elemenTombol = document.getElementById("tombol-kunci");
const elemenCache = document.getElementById("isi-cache");
const elemenRingkasan = document.getElementById("ringkasan");
const elemenLog = document.getElementById("log");
// satu tombol untuk setiap kunci
for (const kunci of KUNCI) {
const tombol = document.createElement("button");
tombol.type = "button";
tombol.textContent = kunci;
tombol.addEventListener("click", () => akses(kunci));
elemenTombol.appendChild(tombol);
}
function akses(kunci) {
const posisi = cache.indexOf(kunci);
let catatan;
if (posisi !== -1) {
hit += 1;
cache.splice(posisi, 1);
cache.push(kunci); // segarkan posisi menjadi yang paling baru
catatan = `<span class="badge hit">CACHE HIT</span> ${kunci}`;
} else {
miss += 1;
let korban = "";
if (cache.length >= KAPASITAS) {
korban = `, menggusur ${cache[0]}`;
cache.shift();
}
cache.push(kunci);
catatan = `<span class="badge miss">CACHE MISS</span> ${kunci} diambil dari basis data${korban}`;
}
tampilkan(catatan);
}
function tampilkan(catatan) {
// isi cache: empat slot, entri paling baru diberi sorotan
elemenCache.textContent = "";
for (let i = 0; i < KAPASITAS; i += 1) {
const slot = document.createElement("div");
slot.className = "slot";
if (cache[i] !== undefined) {
slot.textContent = cache[i];
slot.classList.add("terisi");
if (i === cache.length - 1) slot.classList.add("terbaru");
} else {
slot.textContent = "(kosong)";
}
elemenCache.appendChild(slot);
}
const total = hit + miss;
const rasio = total === 0 ? "-" : `${((hit / total) * 100).toFixed(1)}%`;
elemenRingkasan.textContent =
`Cache hit: ${hit} · Cache miss: ${miss} · Cache hit ratio: ${rasio}`;
const item = document.createElement("li");
item.innerHTML = catatan;
const penandaKosong = elemenLog.querySelector(".kosong");
if (penandaKosong !== null) penandaKosong.remove();
elemenLog.prepend(item);
while (elemenLog.children.length > 8) {
elemenLog.removeChild(elemenLog.lastChild);
}
}
</script>
<script>
// Simulator cache hit dan cache miss dengan penggusuran LRU.
// State disimpan pada tiga variabel sederhana: isi cache, hitungan hit, dan miss.
const KUNCI = [
"profil-ali", "profil-budi", "profil-citra", "profil-dewi",
"profil-edi", "profil-farah", "profil-gita", "profil-hana",
];
const KAPASITAS = 4;
const cache = []; // daftar kunci, dari yang paling lama tidak diakses
let hit = 0;
let miss = 0;
const elemenTombol = document.getElementById("tombol-kunci");
const elemenCache = document.getElementById("isi-cache");
const elemenRingkasan = document.getElementById("ringkasan");
const elemenLog = document.getElementById("log");
// satu tombol untuk setiap kunci
for (const kunci of KUNCI) {
const tombol = document.createElement("button");
tombol.type = "button";
tombol.textContent = kunci;
tombol.addEventListener("click", () => akses(kunci));
elemenTombol.appendChild(tombol);
}
function akses(kunci) {
const posisi = cache.indexOf(kunci);
let catatan;
if (posisi !== -1) {
hit += 1;
cache.splice(posisi, 1);
cache.push(kunci); // segarkan posisi menjadi yang paling baru
catatan = `<span class="badge hit">CACHE HIT</span> ${kunci}`;
} else {
miss += 1;
let korban = "";
if (cache.length >= KAPASITAS) {
korban = `, menggusur ${cache[0]}`;
cache.shift();
}
cache.push(kunci);
catatan = `<span class="badge miss">CACHE MISS</span> ${kunci} diambil dari basis data${korban}`;
}
tampilkan(catatan);
}
function tampilkan(catatan) {
// isi cache: empat slot, entri paling baru diberi sorotan
elemenCache.textContent = "";
for (let i = 0; i < KAPASITAS; i += 1) {
const slot = document.createElement("div");
slot.className = "slot";
if (cache[i] !== undefined) {
slot.textContent = cache[i];
slot.classList.add("terisi");
if (i === cache.length - 1) slot.classList.add("terbaru");
} else {
slot.textContent = "(kosong)";
}
elemenCache.appendChild(slot);
}
const total = hit + miss;
const rasio = total === 0 ? "-" : `${((hit / total) * 100).toFixed(1)}%`;
elemenRingkasan.textContent =
`Cache hit: ${hit} · Cache miss: ${miss} · Cache hit ratio: ${rasio}`;
const item = document.createElement("li");
item.innerHTML = catatan;
const penandaKosong = elemenLog.querySelector(".kosong");
if (penandaKosong !== null) penandaKosong.remove();
elemenLog.prepend(item);
while (elemenLog.children.length > 8) {
elemenLog.removeChild(elemenLog.lastChild);
}
}
</script></body>
</html>Kesalahan umum#
- Cache tanpa batas usia. Penyebabnya, salinan disimpan tanpa usia maksimum sekaligus tanpa pembuangan saat data berubah, sehingga data basi disajikan terus. Perbaikannya, tetapkan usia maksimum pada setiap entri dan buang entri ketika datanya diperbarui.
- Menyimpan respons pribadi pada cache bersama. Penyebabnya, halaman milik satu pengguna tersimpan pada cache CDN atau proxy, yaitu cache perantara yang melayani banyak pengguna, sehingga isinya bocor ke pengguna lain. Perbaikannya, tandai respons pribadi agar tidak disimpan pada cache bersama dan hanya cache pribadi yang boleh menyimpannya.
- Menyimpan ke cache data yang jarang dipakai ulang. Penyebabnya, cache terisi entri yang tidak pernah dibaca lagi, sehingga kapasitasnya habis untuk data yang tidak memberi cache hit. Perbaikannya, prioritaskan data yang sering dibaca dan jarang berubah, lalu amati rasio hitnya sebelum kapasitas cache diperbesar.
Ringkasan#
- Cache hit menjawab permintaan dari salinan yang berlaku, cache miss meneruskan permintaan ke sumber, dan rasio hit mengukur porsi permintaan yang dijawab langsung.
- Permintaan web melewati cache browser, CDN, cache aplikasi, dan cache basis data, dan lapisan pertama yang masih menyimpan salinan valid menghentikan perjalanan permintaan.
- Lapisan-lapisan itu berbeda dalam kepemilikan data, pribadi maupun bersama, serta dalam kendalinya, mulai dari header HTTP, kode aplikasi, hingga pengelolaan otomatis.
- Simulasi LRU memperlihatkan rasio hit 50% pada urutan akses campuran dan peran lokalitas akses dalam menjaga kunci yang populer tetap panas.
Referensi#
- HTTP caching — MDN Web Docs
— Dokumentasi resmi caching HTTP, termasuk cache pribadi dan bersama serta peran header
Cache-Control. - What is caching? — Cloudflare — Penjelasan konsep cache, cache hit dan miss, serta peran CDN dalam menyimpan konten dekat pengguna.