Strategi Cache

Cache-aside versus cache-through, TTL, stampede, dan pola penanggulangannya.

Daftar isi

Lapisan cache hanya berguna bila isinya benar dan cukup segar, dan itulah urusan strategi cache. Topik ini membahas pola cache-aside berbanding cache-through, peran TTL dalam mengatur usia salinan, serta cache stampede beserta pola penanggulangannya. Dua simulasi Python disertakan sehingga setiap klaim tentang perilaku pola tersebut dapat diperiksa ulang dari keluaran aslinya.

Cache-aside dan cache-through#

Cache-aside, yang oleh sebagian dokumentasi disebut lazy loading, menempatkan pengelolaan cache di tangan aplikasi. Saat membaca, aplikasi memeriksa cache lebih dahulu. Pada cache miss, aplikasi mengambil data dari basis data, menulisnya ke cache, lalu mengembalikannya. Saat menulis, aplikasi memperbarui basis data dan membuang entri cache yang bersangkutan agar pembaca berikutnya memuat nilai yang baru. Pola ini paling umum karena tidak menuntut perangkat cache tertentu dan aplikasi memegang kendali penuh.

Cache-through menyerahkan pemuatan dan penulisan kepada lapisan cache. Pada read-through, cache memuat data dari sumber ketika entri tidak ada, sehingga aplikasi hanya berbicara dengan cache. Pada write-through, setiap penulisan diteruskan ke cache dan basis data sebelum respons dikembalikan, sehingga isi cache tidak pernah basi tetapi setiap tulisan membayar dua penulisan. Write-behind menulis ke cache lebih dahulu dan menyimpan ke basis data menyusul secara asinkron, yang mempercepat respons tetapi berisiko kehilangan tulisan terakhir apabila cache mati sebelum penulisan menyusul.

PolaPengelola cacheKelebihanKekurangan
Cache-asideaplikasisederhana, hanya data yang diminta yang tersimpanmiss pertama lambat, data basi hingga TTL habis
Read-throughlapisan cachekode aplikasi tipismenuntut cache yang mendukungnya
Write-throughlapisan cacheisi cache tidak pernah basisetiap tulisan membayar dua penulisan
Write-behindlapisan cacherespons tulisan cepattulisan terakhir dapat hilang saat kegagalan

Implementasi cache-aside dengan TTL#

Simulasi berikut mengikuti pola cache-aside pada tiga kunci pengukuran suhu, dengan TTL (time to live), yaitu usia maksimum entri cache, sebesar 30 detik. Jam simulasi dimajukan secara manual, sehingga kedaluwarsa dapat diamati tanpa menunggu waktu nyata.

python cache-aside.py
# Pola cache-aside dengan TTL (time to live) disimulasikan tanpa pustaka
# tambahan. Aplikasi membaca cache lebih dahulu. Bila kuncinya tidak ada
# atau sudah kedaluwarsa, data diambil dari basis data lalu ditulis kembali
# ke cache sebelum dikembalikan ke pemanggil.

TTL = 30   # usia maksimum entri cache, dalam detik
waktu = 0  # detik simulasi saat ini

basis_data = {"suhu-a1": 24.5, "suhu-a2": 23.1, "suhu-b1": 25.8}
cache = {}  # kunci -> (nilai, kedaluwarsa)


def baca(kunci):
    entri = cache.get(kunci)
    if entri is not None and entri[1] > waktu:
        print(f"t={waktu:>3}  {kunci:<8} CACHE HIT                 nilai={entri[0]}")
        return
    status = "CACHE MISS (kedaluwarsa)" if entri else "CACHE MISS (belum ada)"
    nilai = basis_data[kunci]
    cache[kunci] = (nilai, waktu + TTL)
    print(f"t={waktu:>3}  {kunci:<8} {status:<24} nilai={nilai}  (basis data dikunjungi)")


URUTAN_AKSES = [
    ("suhu-a1", 0), ("suhu-a1", 10), ("suhu-a2", 20), ("suhu-a1", 40),
    ("suhu-a1", 50), ("suhu-b1", 60), ("suhu-a1", 75),
]

for kunci, saat in URUTAN_AKSES:
    waktu = saat
    baca(kunci)
text Keluaran
t=  0  suhu-a1  CACHE MISS (belum ada)   nilai=24.5  (basis data dikunjungi)
t= 10  suhu-a1  CACHE HIT                 nilai=24.5
t= 20  suhu-a2  CACHE MISS (belum ada)   nilai=23.1  (basis data dikunjungi)
t= 40  suhu-a1  CACHE MISS (kedaluwarsa) nilai=24.5  (basis data dikunjungi)
t= 50  suhu-a1  CACHE HIT                 nilai=24.5
t= 60  suhu-b1  CACHE MISS (belum ada)   nilai=25.8  (basis data dikunjungi)
t= 75  suhu-a1  CACHE MISS (kedaluwarsa) nilai=24.5  (basis data dikunjungi)

Alur cache-aside terbaca lengkap pada tujuh akses tersebut. Akses pertama pada t=0 mengunjungi basis data lalu mengisi cache, akses pada t=10 dijawab cache, dan akses pada t=40 mengunjungi basis data lagi karena entri yang dibuat pada t=0 telah melewati usia 30 detiknya. Perhatikan bahwa kedaluwarsa memberi jaminan berupa pembatasan usia salinan, bukan jaminan kesegaran: apabila nilai suhu berubah pada t=15, pembaca tetap menerima nilai lama sampai entri berakhir pada t=30.

Cache stampede dan penanggulangannya#

Cache stampede terjadi ketika kunci yang ramai kedaluwarsa dan sejumlah besar permintaan tiba bersamaan, semuanya menemukan cache kosong, lalu semuanya mengunjungi basis data untuk menghitung ulang nilai yang sama. Lonjakan itu muncul bukan karena trafik naik, melainkan karena kedaluwarsa, dan bisa lebih berat daripada beban normal.

Simulasi berikut menguji dua bentuk stampede. Skenario pertama mengirimkan 40 permintaan untuk satu kunci panas dalam lima detik setelah kunci kedaluwarsa, dengan hitung ulang yang memakan waktu lima detik, lalu membandingkan kondisi tanpa penanggul dengan kondisi single-flight, yaitu hanya satu permintaan yang menghitung ulang dan sisanya menunggu. Skenario kedua membuat 30 kunci sekaligus dengan TTL yang sama, lalu membandingkannya dengan versi yang diberi jitter, yaitu penggeseran acak pada TTL agar kedaluwarsa tidak terjadi serentak.

python simulasi-stampede.py
# Simulasi cache stampede beserta penanggulnya.
# Permintaan dimodelkan sebagai daftar detik kedatangan. Selama entri cache
# belum tersedia kembali, setiap permintaan yang tidak menunggu harus
# mengambil data dari basis data (dihitung sebagai kunjungan).

DURASI_HITUNG_ULANG = 5  # detik yang dibutuhkan untuk menghitung ulang data panas


def skenario_kunci_tunggal(single_flight):
    """40 permintaan datang dalam lima detik setelah kunci panas kedaluwarsa."""
    kedatangan = [300 + (i * 5) // 40 for i in range(40)]
    valid_sejak = 0              # entri awal berlaku sejak detik 0
    valid_hingga = 300           # dan kedaluwarsa tepat pada detik 300
    recompute_selesai = None     # detik ketika hitung ulang yang berjalan selesai
    kunjungan_db = 0
    menunggu = 0
    for t in kedatangan:
        if valid_sejak <= t < valid_hingga:
            continue  # cache hit
        # entri kedaluwarsa: cache kosong hingga hasil hitung ulang tersedia
        if single_flight and recompute_selesai is not None and t < recompute_selesai:
            menunggu += 1  # hitung ulang sedang berjalan: permintaan menunggu
            continue
        kunjungan_db += 1  # cache miss: kunjungan basis data
        recompute_selesai = t + DURASI_HITUNG_ULANG
        # entri baru hanya tersedia setelah hitung ulang selesai
        valid_sejak = t + DURASI_HITUNG_ULANG
        valid_hingga = valid_sejak + 300
    return kunjungan_db, menunggu


def skenario_banyak_kunci(jitter):
    """30 kunci dibuat bersamaan; hitung kunjungan basis data per detik."""
    import random

    random.seed(42)
    kedaluwarsa = {}
    for k in range(30):
        geser = random.randint(0, 60) if jitter else 0
        kedaluwarsa[f"kunci-{k:02d}"] = 300 + geser
    kunjungan_per_detik = {}
    for detik in kedaluwarsa.values():
        kunjungan_per_detik[detik] = kunjungan_per_detik.get(detik, 0) + 1
    return min(kedaluwarsa.values()), max(kedaluwarsa.values()), max(kunjungan_per_detik.values())


tanpa, tunggu = skenario_kunci_tunggal(single_flight=False)
dengan, menunggu = skenario_kunci_tunggal(single_flight=True)
print("SKENARIO 1: satu kunci panas kedaluwarsa, 40 permintaan datang dalam 5 detik")
print(f"  tanpa penanggul      : {tanpa} kunjungan basis data")
print(f"  dengan single-flight : {dengan} kunjungan basis data, {menunggu} permintaan menunggu")

awal, akhir, puncak_jitter = skenario_banyak_kunci(jitter=True)
_, _, puncak_seragam = skenario_banyak_kunci(jitter=False)
print("SKENARIO 2: 30 kunci dengan TTL seragam dibuat pada waktu yang sama")
print(f"  tanpa jitter : seluruh 30 entri kedaluwarsa pada detik 300, puncak {puncak_seragam} kunjungan per detik")
print(f"  dengan jitter: kedaluwarsa tersebar pada detik {awal}-{akhir}, puncak {puncak_jitter} kunjungan per detik")
text Keluaran
SKENARIO 1: satu kunci panas kedaluwarsa, 40 permintaan datang dalam 5 detik
  tanpa penanggul      : 40 kunjungan basis data
  dengan single-flight : 1 kunjungan basis data, 39 permintaan menunggu
SKENARIO 2: 30 kunci dengan TTL seragam dibuat pada waktu yang sama
  tanpa jitter : seluruh 30 entri kedaluwarsa pada detik 300, puncak 30 kunjungan per detik
  dengan jitter: kedaluwarsa tersebar pada detik 301-357, puncak 3 kunjungan per detik

Kedua skenario menunjukkan penanggul yang berbeda untuk dua bentuk stampede yang berbeda. Pada kunci tunggal, seluruh 40 permintaan tiba sebelum hitung ulang pertama selesai, sehingga tanpa penanggul basis data dikunjungi 40 kali, sedangkan single-flight menurunkannya menjadi satu kunjungan dengan 39 permintaan yang menunggu hasil yang sama. Pada kumpulan kunci, TTL seragam membuat seluruh 30 entri kedaluwarsa pada detik yang sama, sedangkan jitter menyebarkannya ke rentang 57 detik sehingga puncaknya hanya tiga kunjungan per detik. Pola ketiga yang melengkapi keduanya adalah pembaruan awal, yaitu entri panas dihitung ulang sebelum kedaluwarsa, sehingga cache tidak pernah kosong.

Memilih pola sesuai pola akses#

Pemilihan pola mengikuti dua sifat data: perbandingan baca dan tulis, serta toleransi terhadap data basi. Data yang banyak dibaca, jarang berubah, dan boleh basi sejenak paling cocok dengan cache-aside yang bertumpu pada TTL, karena pola ini sederhana dan hanya menyimpan data yang benar-benar diminta. Data yang harus segar setelah setiap penulisan membutuhkan write-through, yang membayar dua penulisan demi konsistensi cache dan basis data. Data yang padat ditulis dan boleh menunggu sebentar disimpan lewat write-behind dengan risikonya diperhitungkan.

Kombinasi yang paling sering ditemui pada produksi (production) adalah cache-aside dengan TTL, pembuangan entri saat data ditulis, dan single-flight untuk kunci yang diketahui panas. Ketiganya saling menutup celah: TTL membatasi usia salinan, pembuangan menjaga kesegaran setelah tulis, dan single-flight menjaga basis data dari lonjakan kedaluwarsa.

Kesalahan umum#

  1. Memperbarui basis data tanpa membuang entri cache. Penyebabnya, jalur tulis menganggap TTL akan mengurus semuanya, padahal pembaca tetap menerima nilai lama sampai entri berakhir. Perbaikannya, buang entri cache yang bersangkutan pada setiap penulisan, sehingga pembaca berikutnya memuat nilai baru.
  2. Memberi TTL seragam pada kunci yang dibuat bersamaan. Penyebabnya, kedaluwarsa bertepukan dan memicu stampede seperti pada skenario kedua di atas. Perbaikannya, tambahkan penggeseran acak pada TTL setiap kunci.
  3. Memperlakukan cache sebagai satu-satuan salinan. Penyebabnya, data penting hanya tersimpan di cache yang memang dirancang untuk membuang isinya, sehingga kegagalan cache berarti kehilangan data. Perbaikannya, tulis data penting ke basis data terlebih dahulu dan perlakukan cache sebagai salinan yang boleh kosong kapan saja.

Ringkasan#

  • Cache-aside menyerahkan pengelolaan cache kepada aplikasi dengan alur baca-miss-muat-simpan, sedangkan read-through, write-through, dan write-behind menyerahkannya kepada lapisan cache dengan biaya penulisan dan risiko kehilangan masing-masing.
  • TTL membatasi usia salinan, bukan menjamin kesegaran, sehingga penulisan data tetap perlu disertai pembuangan entri cache yang bersangkutan.
  • Stampede muncul ketika kedaluwarsa bertepukan dengan permintaan yang ramai, dan penanggulnya berupa single-flight untuk kunci tunggal, jitter untuk kumpulan kunci, serta pembaruan awal untuk entri yang tidak boleh kosong.

Referensi#