Caching

Cache-aside dengan redis-py: pembacaan, pengisian, TTL, dan mitigasi cache stampede.

Daftar isi

Cache-aside adalah pola caching yang mengelola isi cache dari sisi aplikasi, dengan Redis dipanggil lebih dahulu sebelum sumber data dijangkau. Aplikasi membaca cache terlebih dahulu, membaca sumber data hanya apabila cache kosong, lalu menulis kembali hasilnya beserta masa hidup terbatas. Topik ini membahas alur pola cache-aside, implementasinya dengan redis-py, pemeriksaan key cache lewat redis-cli, pemilihan durasi TTL, serta pola pikir mitigasi cache stampede. Contoh dijalankan pada server Redis 8.0.2 di port 6379 menggunakan key berawalan cache:, sehingga setiap demo dapat dijalankan ulang dengan keluaran yang sama.

Pola cache-aside#

Pola cache-aside menetapkan urutan baca yang sama untuk setiap permintaan data. Aplikasi membaca key cache terlebih dahulu dengan perintah GET. Apabila nilainya ada, kondisi itu disebut cache hit, dan permintaan selesai tanpa menyentuh sumber data. Apabila nilainya tidak ada atau sudah kedaluwarsa, kondisi itu disebut cache miss, dan aplikasi melanjutkan dengan dua langkah, yaitu membaca data dari sumber lalu menulis hasilnya ke cache dengan SET beserta masa hidup (time to live, TTL) pada perintah yang sama. Pembacaan berikutnya atas data yang sama mengalami cache hit karena salinannya sudah berada di Redis.

Cache pada pola ini menyimpan salinan, bukan acuan kebenaran. Data yang asli tetap berada di sumber, sehingga hilangnya satu salinan cache tidak menghilangkan data. Konsekuensinya, salinan diizinkan basi selama durasi TTL-nya, dan kebasian itulah yang ditukar dengan kecepatan pembacaan. Perilaku ini berbeda dari pola write-through, yang menulis ke cache pada setiap penulisan data sehingga isi cache selalu mengikuti sumber.

Diagram berikut merangkum alur tersebut, dari pembacaan pertama yang mengalami miss sampai pembacaan berikutnya yang langsung dijawab Redis.

Aplikasiredis-pyRediscache:produk:1750Sumber databasis data1 GET cachecache hit2 cache missbaca sumber3 data dikembalikan, cache ditulis dengan TTL
Alur cache-aside: baca cache, baca sumber data saat miss, tulis kembali dengan TTL, lalu cache hit pada pembacaan berikutnya

Implementasi cache-aside dengan redis-py#

Skrip berikut mengimplementasikan alur tersebut dalam satu fungsi bernama get_produk. Sumber data disimulasikan sebagai dictionary Python, sedangkan pada aplikasi nyata langkah pembacaan sumbernya adalah query ke basis data, panggilan API, atau penghitungan yang mahal. Fungsi membaca key cache:produk:1750 dengan method get, dan nilai kembalian None menjadi penanda cache miss. Pada cabang miss, fungsi membaca sumber data, mengubah data produk menjadi string JSON, lalu menulisnya dengan r.set beserta opsi ex=600. Opsi ex memasang masa hidup dalam satuan detik pada perintah SET yang sama, sehingga penulisan nilai dan pemasangan masa hidup berjalan atomik.

python cache_aside.py
import json
import redis

# bersihkan state demo agar alur miss lalu hit selalu sama
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
r.delete("cache:produk:1750")

# sumber data yang disimulasikan; pada aplikasi nyata sumbernya adalah basis data
stok_produk = {
    1750: {"id": 1750, "nama": "Kalkulator Ilmiah", "stok": 24},
}

def get_produk(produk_id):
    cache_key = f"cache:produk:{produk_id}"
    nilai_cache = r.get(cache_key)
    if nilai_cache is not None:
        print("cache hit")
        return json.loads(nilai_cache)
    print("cache miss")
    produk = stok_produk[produk_id]
    r.set(cache_key, json.dumps(produk), ex=600)
    print("cache diisi dari sumber data")
    return produk

# pembacaan pertama: cache masih kosong
print(get_produk(1750))
# pembacaan kedua: nilai sudah ada di cache
print(get_produk(1750))
text Keluaran
cache miss
cache diisi dari sumber data
{'id': 1750, 'nama': 'Kalkulator Ilmiah', 'stok': 24}
cache hit
{'id': 1750, 'nama': 'Kalkulator Ilmiah', 'stok': 24}

Keluaran memperlihatkan alur lengkap dalam dua pembacaan. Pembacaan pertama mengalami cache miss karena key belum ada, sehingga skrip mencetak cache miss, mengisi cache dari sumber data, dan mengembalikan data produk. Pembacaan kedua mengalami cache hit karena salinan yang baru ditulis sudah berada di Redis, dan nilainya identik dengan pembacaan pertama karena dibaca dari string JSON yang sama.

Pemeriksaan key cache lewat redis-cli#

Setelah skrip dijalankan, key cache:produk:1750 tersimpan di server dengan masa hidup 600 detik. Isinya dapat diperiksa dengan perintah GET, sedangkan sisa masa hidupnya dibaca dengan perintah TTL. Kedua perintah berikut mengasumsikan skrip Python sebelumnya baru saja dijalankan, sehingga key yang diperiksa adalah key yang baru diisi. Pemeriksaan yang sama dapat diulang kapan pun, karena skrip selalu membersihkan key yang sama sebelum mengisinya ulang.

bash
# membaca nilai cache yang ditulis skrip Python sebelumnya
redis-cli GET cache:produk:1750
# membaca sisa masa hidup cache yang baru diisi
redis-cli TTL cache:produk:1750
text Keluaran
{"id": 1750, "nama": "Kalkulator Ilmiah", "stok": 24}
600

GET menampilkan string JSON persis seperti yang ditulis skrip, sedangkan TTL melaporkan 600 karena pemeriksaan dilakukan seketika setelah pengisian. Nilai 600 tersebut sekaligus mengonfirmasi bahwa opsi ex=600 benar diterapkan pada saat penulisan, karena key yang ditulis tanpa masa hidup akan dilaporkan -1 oleh TTL. Apabila pemeriksaan ditunda beberapa detik, angka tersebut bergerak turun bertahap sampai key kedaluwarsa. Key yang sudah kedaluwarsa diperlakukan sebagai key yang tidak ada, sehingga pembacaan atasnya kembali mengalami cache miss dan siklus pengisian berulang dari awal.

Memilih durasi TTL#

Durasi TTL menentukan dua hal sekaligus, yaitu batas kebasian salinan dan frekuensi pembacaan sumber data. Durasi yang pendek membuat salinan selalu segar, tetapi cache miss terjadi lebih sering sehingga sumber data lebih sibuk. Durasi yang panjang mengurangi jumlah pembacaan sumber, tetapi salinan berpotensi basi lebih lama. Pilihan yang tepat dihitung dari dua faktor, yaitu laju perubahan data pada sumber dan toleransi kebasian aplikasi yang membacanya.

Jenis dataPerubahan pada sumberTitik awal durasi
Konfigurasi aplikasijarang berubahberjam-jam hingga berhari-hari
Statistik nilai ujiandiperbarui berkalapuluhan menit
Ketersediaan kursi loketberubah cepatpuluhan detik

Angka pada tabel hanyalah titik awal, bukan aturan, karena durasi akhirnya mengikuti pengamatan atas data yang sesungguhnya. Satu pola perubahan juga patut diwaspadai. Apabila banyak key diisi pada waktu yang hampir bersamaan dengan masa hidup seragam, seluruhnya kedaluwarsa berdekatan pula, dan keadaan inilah yang biasanya memicu cache stampede.

Risiko cache stampede#

Cache stampede adalah situasi ketika sebuah key yang populer kedaluwarsa, lalu banyak permintaan bersamaan mengalami cache miss atas key yang sama pada saat itu juga. Semua permintaan tersebut melanjutkan ke sumber data untuk nilai yang sama, sehingga sumber mendapat lonjakan pembacaan yang biasanya tidak dialaminya. Lonjakan semacam ini terutama merugikan key yang dibaca berulang kali dalam interval singkat.

Tiga mitigasi umum dapat dipertimbangkan tanpa harus mengubah struktur data yang sudah ada. Pertama, lock singkat saat mengisi, yaitu satu proses mengambil lock dengan SET beserta opsi NX dan masa hidup pendek, lalu hanya proses yang menang membaca sumber data, sedangkan proses lain mencoba membaca cache lagi sesaat kemudian. Dokumentasi resmi Redis mencontohkan pendekatan ini sebagai proteksi stampede. Kedua, jitter TTL, yaitu menambahkan variasi acak kecil pada durasi sehingga key yang diisi serentak tidak kedaluwarsa serentak, dengan perubahan kode yang kecil. Ketiga, stale-while-revalidate, yaitu menyajikan salinan lama yang sudah kedaluwarsa sambil satu permintaan menyegarkan nilainya, pendekatan yang menuntut penyimpanan salinan lama dan penanda penyegaran pada cache.

Ketiga mitigasi menambah kompleksitas pada aplikasi, sehingga pilihannya disesuaikan dengan skala beban. Untuk beban menengah, jitter TTL biasanya sudah cukup karena perubahannya kecil dan tidak menambah perintah baru pada jalur pembacaan. Lock dan stale-while-revalidate baru menunjukkan manfaatnya apabila satu key benar-benar dibaca banyak proses pada saat yang sama.

Ringkasan#

  • Cache-aside membaca cache terlebih dahulu, membaca sumber data hanya pada cache miss, lalu menulis kembali hasilnya dengan masa hidup terbatas.
  • Pada redis-py, GET yang mengembalikan None menjadi penanda cache miss, sedangkan SET dengan opsi ex memasang nilai dan masa hidup secara atomik.
  • Durasi TTL menyeimbangkan kebasian salinan dengan frekuensi pembacaan sumber data, dan dipilih dari laju perubahan data serta toleransi kebasian aplikasi.
  • Cache stampede muncul ketika banyak permintaan bersamaan mengalami miss atas key yang sama, dengan mitigasi berupa lock singkat saat mengisi, jitter TTL, atau stale-while-revalidate.

Referensi#