Memilih antara local in-memory cache (seperti Caffeine di Java, sync.Map di Go, atau node-cache di Node.js) dan distributed cache (Redis atau KeyDB) menentukan profil latensi, konsistensi data, dan biaya infrastruktur sistem backend. Pilihan ini bukan sekadar preferensi arsitektur, melainkan kompromi langsung antara kecepatan akses memori lokal melawan konsistensi status global di lingkungan multi-instance.
1. Latensi Akses: Direct RAM vs Network Roundtrip (RTT)
Perbedaan performa mendasar antara in-memory lokal dan Redis terletak pada batas fisik transmisi data.
- In-Memory Cache Lokal: Operasi baca/tulis dieksekusi langsung pada alokasi RAM proses aplikasi (L1/L2/L3 CPU cache hingga main memory). Waktu akses berada di rentang puluhan hingga ratusan nanodetik (sub-microsecond). Tidak ada proses serialisasi objek jika runtime mendukung penyimpanan pointer/referensi objek langsung.
- Redis (Distributed Cache): Operasi memerlukan IPC (Unix Domain Socket) atau network hop via TCP/IP. Walaupun Redis mengeksekusi perintah di RAM dalam hitungan mikrodetik, latensi total didominasi oleh network roundtrip time (RTT), serialisasi data (JSON/Protobuf/RESP), alokasi buffer socket, dan context switching OS. Latensi tipikal berkisar antara 0.5 ms hingga 2 ms pada jaringan lokal cloud (AWS VPC / GCP VPC).
Jika sebuah endpoint API mengeksekusi 20 cache lookups secara serial per request, local cache menghabiskan ~2-10 mikrodetik, sedangkan Redis menghabiskan minimal 10-40 milidetik hanya untuk network overhead, kecuali dioptimasi menggunakan pipeline atau MGET.
2. Konsistensi Data dan Masalah Invalidation Drift
Saat aplikasi diskalakan secara horizontal (N instance container di Kubernetes atau VM), cache lokal memecah single source of truth.
Setiap container menyimpan salinan datanya sendiri. Ketika Instance A mengubah data di database dan menghapus cache lokalnya, Instance B dan C tetap menyimpan salinan lama (stale data). Fenomena ini disebut invalidation drift. Data menjadi tidak konsisten antar-user tergantung pada instance mana yang menerima traffic dari load balancer.
Client Update -> [Pod A] (Local Cache Evicted, DB Updated)
Client Read -> [Pod B] (Returns Stale Local Cache) -> Invalidation Drift
Client Read -> [Pod A] (Returns Fresh Data from DB)Redis menyelesaikan masalah ini dengan memusatkan status data. Semua pod membaca dan menulis ke instance atau cluster Redis yang sama. Sekali kunci dihapus atau diperbarui di Redis, seluruh instance aplikasi langsung melihat status data terbaru.
3. Konsumsi RAM Container vs Biaya Infrastruktur Redis
Trade-off finansial dan efisiensi resource membedakan kedua pendekatan ini secara signifikan:
Duplikasi Memori pada In-Memory Cache
Jika dataset cache berukuran 500 MB dan aplikasi berjalan di 20 pod, total RAM yang dikonsumsi untuk cache di cluster adalah 10 GB (500 MB × 20). Data yang sama diduplikasi 20 kali. Hal ini meningkatkan baseline batas memori container (cgroup memory limit) dan memperbesar risiko container dimatikan oleh OS via OOM Killer.
Overhead Biaya Redis Terkelola
Redis memusatkan data sehingga memori hanya digunakan 1 kali (500 MB terlepas dari berapa banyak pod aplikasi yang berjalan). Namun, menjalankan Redis membutuhkan biaya infrastruktur terpisah: instance VM mandiri atau layanan terkelola (seperti AWS ElastiCache, GCP Memorystore). Layanan terkelola dengan kapabilitas high-availability (Multi-AZ replication, failover otomatis) memiliki biaya minimum bulanan yang jauh lebih tinggi dibanding memanfaatkan sisa alokasi RAM container yang sudah ada.
4. Pola Two-Tier Caching (L1 Local + L2 Redis)
Untuk menggabungkan latensi sub-mikrodetik dari cache lokal dengan sentralisasi Redis, arsitektur two-tier caching sering diimplementasikan: L1 berupa in-memory cache lokal, dan L2 berupa Redis terdistribusi.
package main
import (
"context"
"sync"
"time"
"github.com/redis/go-redis/v9"
)
type TwoTierCache struct {
l1 sync.Map // In-memory local cache
l2 *redis.Client
ctx context.Context
}
func (c *TwoTierCache) Get(key string) (string, error) {
// 1. Cek L1 (Direct RAM access: nanoseconds)
if val, ok := c.l1.Load(key); ok {
return val.(string), nil
}
// 2. Fallback ke L2 Redis (Network RTT: 0.5-2ms)
val, err := c.l2.Get(c.ctx, key).Result()
if err != nil {
return "", err
}
// 3. Populate L1 lokal dengan short TTL
c.l1.Store(key, val)
return val, nil
}
func (c *TwoTierCache) Invalidate(key string) error {
// Hapus L2 dan publish event ke pod lain
c.l2.Del(c.ctx, key)
return c.l2.Publish(c.ctx, "cache:invalidate", key).Err()
}
func (c *TwoTierCache) ListenInvalidations() {
pubsub := c.l2.Subscribe(c.ctx, "cache:invalidate")
ch := pubsub.Channel()
for msg := range ch {
// Hapus entri L1 lokal saat menerima sinyal
c.l1.Delete(msg.Payload)
}
}
Risiko Sinkronisasi via Redis Pub/Sub
Mekanisme sinkronisasi invalidasi via Redis Pub/Sub di atas memiliki kerentanan teknis signifikan:
- At-Most-Once Delivery: Redis Pub/Sub bersifat fire-and-forget. Jika sebuah pod mengalami network blip, reconnecting, atau GC pause panjang saat pesan invalidasi dikirim, pesan tersebut hilang selamanya untuk pod tersebut. Pod tersebut akan menyajikan data basi sampai instance di-restart.
- Mitigasi Wajib: Tetapkan batas TTL pendek pada cache L1 (misalnya 10 hingga 60 detik) sebagai batas maksimal toleransi data basi jika sinyal invalidasi gagal diterima.
5. Matriks Perbandingan Teknis
| Kriteria | In-Memory Lokal (L1) | Redis / KeyDB (L2) |
|---|---|---|
| Latensi Rata-rata | < 1 mikrodetik (RAM dereference) | 0.5 ms – 2 ms (Network hop + RESP) |
| Konsistensi Multi-Node | Rendah (Rawan Invalidation Drift) | Tinggi (Single source of truth) |
| Penggunaan Memori | Redundan (Tergantung jumlah replika pod) | Efisien (Tersentralisasi di cluster cache) |
| Ketahanan Restart | Hilang total saat container restart | Dapat persisten (RDB / AOF snapshot) |
| Kompleksitas Operasional | Nol (Hanya library di level kode) | Tinggi (Clustering, Sentinel, patching, backup) |
| Impact Garbage Collection | Tinggi (Objek cache membebani runtime GC) | Nol terhadap GC aplikasi backend |
6. Kriteria Pengambilan Keputusan
Gunakan In-Memory Cache Lokal Jika:
- Aplikasi beroperasi sebagai single-instance atau monolith sederhana tanpa horizontal replica.
- Data bersifat immutable atau statis (contoh: konfigurasi sistem, data referensi wilayah, kamus i18n).
- Sistem membutuhkan throughput pembacaan sangat tinggi (puluhan ribu RPS per node) yang jika dialihkan ke network akan membebani bandwidth kartu jaringan atau Redis single thread.
- Aplikasi mentoleransi pembacaan data yang sedikit usang (stale-while-revalidate) asalkan dibatasi oleh TTL pendek (10-30 detik).
Wajib Beralih ke Redis Jika:
- Aplikasi diskalakan horizontal di Kubernetes/multi-container dan membutuhkan konsistensi data absolut antar request.
- Cache digunakan untuk koordinasi state bersama: Distributed Locks (Redlock), Centralized Rate Limiting, atau Shared User Sessions.
- Ukuran dataset cache besar (> beberapa Gigabyte), di mana duplikasi memori di setiap pod akan memicu pemborosan biaya node k8s atau resiko OOM kill.
- Cache harus tetap bertahan (survive) saat service aplikasi di-deploy ulang atau mengalami crash.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!