1. Pengenalan Redis
Redis (Remote Dictionary Server) adalah database in-memory yang berfungsi sebagai cache, message broker, dan data store berperforma tinggi. Dibuat oleh Salvatore Sanfilippo pada tahun 2009, Redis mampu menangani jutaan operasi per detik dengan latensi sub-milidetik karena seluruh data disimpan di RAM.
Redis bukan sekadar cache — Redis mendukung berbagai tipe data struktural (string, list, set, sorted set, hash), pub/sub messaging, Lua scripting, transaksi, dan bahkan persistence ke disk. Perusahaan besar seperti Twitter, GitHub, Stack Overflow, dan Snapchat menggunakan Redis di production.
Mengapa Redis Begitu Cepat?
| Faktor | Penjelasan |
|---|---|
| In-Memory | Seluruh data disimpan di RAM — akses data ~100.000x lebih cepat dari disk SSD |
| Single-Threaded | Tidak ada overhead locking/context switching — memanfaatkan event loop efisien |
| I/O Multiplexing | Menggunakan epoll/kqueue untuk menangani ribuan koneksi secara paralel |
| Data Structure Optimized | Struktur data dioptimalkan secara khusus — bukan general-purpose seperti HashMap |
| Protocol Sederhana | RESP (Redis Serialization Protocol) sangat ringan dan cepat di-parse |
Redis vs Alternatif Lain
| Aspek | Redis | Memcached | Varnish |
|---|---|---|---|
| Data Types | Beragam (string, list, set, hash, sorted set) | Hanya string | HTTP objects |
| Persistence | ✅ RDB + AOF | ❌ | ✅ Disk-backed |
| Pub/Sub | ✅ | ❌ | ❌ |
| Clustering | ✅ Built-in | Client-side | ✅ |
| Scripting | ✅ Lua | ❌ | VCL |
| Cocok untuk | Cache, session, queue, analytics | Simple key-value cache | HTTP cache/reverse proxy |
┌─────────────────────────────────────────────────────────────────┐ │ ARSITEKTUR DENGAN REDIS │ │ │ │ ┌─────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Client │─────►│ App │─────►│ Database │ │ │ │(Browser)│◄─────│ Server │◄─────│ (MySQL/ │ │ │ └─────────┘ │(Node.js/ │ │ Postgres)│ │ │ │ Python) │ └──────────┘ │ │ └────┬─────┘ │ │ │ │ │ ┌────▼─────┐ │ │ │ REDIS │ ← In-memory cache │ │ │ │ ← Session store │ │ │ • Cache │ ← Pub/Sub broker │ │ │ • Sess │ ← Rate limiter │ │ │ • Queue │ ← Job queue │ │ └──────────┘ │ │ │ │ Client ──► Cek Redis (cache hit?) ──► Ya: Return cached │ │ ──► Tidak: Query DB, │ │ simpan di Redis │ └─────────────────────────────────────────────────────────────────┘
2. Instalasi & Setup Redis
Docker (Rekomendasi untuk Development)
# Jalankan Redis dengan Docker docker run -d \ --name redis \ -p 6379:6379 \ -v redis_data:/data \ redis:7-alpine \ redis-server --appendonly yes --requirepass "rahasia123" # Masuk ke Redis CLI docker exec -it redis redis-cli -a "rahasia123" # Test koneksi PING # Output: PONG # Cek info server INFO server
Instalasi Lokal
# Ubuntu / Debian sudo apt update sudo apt install redis-server # Konfigurasi password sudo nano /etc/redis/redis.conf # Tambahkan: requirepass your_password # Jalankan service sudo systemctl start redis-server sudo systemctl enable redis-server # macOS dengan Homebrew brew install redis brew services start redis # Test koneksi redis-cli PING # Output: PONG
Koneksi dari Aplikasi
// Instalasi: npm install ioredis
const Redis = require('ioredis');
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'rahasia123',
db: 0,
retryStrategy: (times) => {
if (times > 3) return null; // Stop retry setelah 3x
return Math.min(times * 200, 2000); // Exponential backoff
}
});
redis.on('connect', () => console.log('✅ Terhubung ke Redis'));
redis.on('error', (err) => console.error('❌ Redis error:', err.message));
// Test koneksi
async function main() {
const pong = await redis.ping();
console.log('Redis response:', pong); // PONG
}
main();
3. Redis Data Types
Redis mendukung berbagai tipe data yang jauh lebih kaya dibanding key-value store biasa. Setiap tipe data memiliki operasi khusus yang sangat efisien karena dioptimalkan di level memori.
String — Tipe Dasar
# String sederhana SET user:1:name "Budi Santoso" GET user:1:name # "Budi Santoso" # String dengan expiry (detik) SET otp:08123456789 "938271" EX 300 # Hapus setelah 5 menit TTL otp:08123456789 # 296 (sisa detik) # Increment / Decrement (atomic — aman untuk concurrent) SET page:views 0 INCR page:views # 1 INCR page:views # 2 INCRBY page:views 10 # 12 DECR page:views # 11 # Multiple operations MSET user:1:email "***@example.com" user:1:age "25" MGET user:1:name user:1:email user:1:age # Set if Not Exists (untuk distributed lock) SET lock:order:123 "worker-1" NX EX 30 # Hanya berhasil jika key belum ada, expired 30 detik # Append string SET counter:log "2026-06-25: started" APPEND counter:log " - processing" GET counter:log # "2026-06-25: started - processing"
Hash — Object/Map
# Hash — seperti object/map, cocok untuk data user HSET user:1 name "Budi" email "***@example.com" age 25 city "Jakarta" # Ambil satu field HGET user:1 name # "Budi" # Ambil semua field HGETALL user:1 # 1) "name" # 2) "Budi" # 3) "email" # 4) "budi@example.com" # 5) "age" # 6) "25" # Update field tertentu HSET user:1 age 26 # Update umur HINCRBY user:1 age 1 # Tambah umur +1 # Ambil beberapa field sekaligus HMGET user:1 name email city # Cek apakah field ada HEXISTS user:1 phone # 0 (tidak ada) HSET user:1 phone "08123456789" HEXISTS user:1 phone # 1 (ada) # Hapus field HDEL user:1 phone # Ambil semua keys atau values HKEYS user:1 # ["name", "email", "age", "city"] HVALS user:1 # ["Budi", "budi@example.com", "26", "Jakarta"] HLEN user:1 # 4 (jumlah fields)
List — Queue / Stack
# List — ordered, bisa duplikat, cocok untuk queue/stack LPUSH notifications:user1 "Pesan baru dari Ani" LPUSH notifications:user1 "Order #123 sudah dikirim" RPUSH notifications:user1 "Promo spesial hari ini" # Ambil semua elemen (0 sampai -1 = semua) LRANGE notifications:user1 0 -1 # 1) "Order #123 sudah dikirim" # 2) "Pesan baru dari Ani" # 3) "Promo spesial hari ini" # Pop dari kiri (FIFO queue) — ambil & hapus LPOP notifications:user1 # "Order #123 sudah dikirim" # Blocking pop — tunggu sampai ada data (untuk worker queue) BRPOP task_queue 30 # Tunggu max 30 detik # Panjang list LLEN notifications:user1 # 2 # Ambil elemen tertentu LINDEX notifications:user1 0 # Elemen pertama # Trim — simpan hanya 100 elemen terbaru LTRIM notifications:user1 0 99
Set & Sorted Set
# Set — unordered, unik (tidak ada duplikat) SADD user:1:tags "python" "javascript" "redis" "docker" SADD user:2:tags "python" "golang" "kubernetes" # Cek keanggotaan SISMEMBER user:1:tags "python" # 1 (ya) SISMEMBER user:1:tags "golang" # 0 (tidak) # Set operations SINTER user:1:tags user:2:tags # ["python"] (irisan) SUNION user:1:tags user:2:tags # Semua tag unik gabungan SDIFF user:1:tags user:2:tags # ["javascript","redis","docker"] # Jumlah anggota SCARD user:1:tags # 4 # =================================================== # Sorted Set — seperti Set tapi punya score (untuk ranking) ZADD leaderboard 95 "Budi" ZADD leaderboard 87 "Ani" ZADD leaderboard 99 "Citra" ZADD leaderboard 72 "Dedi" # Ambil ranking (dari score terendah ke tertinggi) ZRANGE leaderboard 0 -1 WITHSCORES # 1) "Dedi" 2) "72" # 3) "Ani" 4) "87" # 5) "Budi" 6) "95" # 7) "Citra" 8) "99" # Top 3 (descending) ZREVRANGE leaderboard 0 2 WITHSCORES # 1) "Citra" 2) "99" # 3) "Budi" 4) "95" # 5) "Ani" 6) "87" # Ambil rank seseorang ZREVRANK leaderboard "Budi" # 1 (0-indexed, #2) ZSCORE leaderboard "Budi" # "95" # Increment score ZINCRBY leaderboard 5 "Ani" # Ani: 87 → 92 # Hitung dalam range score ZCOUNT leaderboard 80 100 # 3 (Ani, Budi, Citra)
4. Pub/Sub Messaging
Pub/Sub (Publish/Subscribe) adalah messaging pattern di mana pengirim (publisher) tidak mengirim pesan langsung ke penerima — melainkan mengirim ke sebuah channel. Siapapun yang subscribe ke channel tersebut akan menerima pesan. Ini memungkinkan komunikasi yang loosely coupled antar komponen sistem.
- Notifikasi real-time — Push notifikasi ke client yang terhubung
- Chat system — Broadcast pesan ke semua anggota room
- Cache invalidation — Notifikasi ke semua server bahwa cache sudah berubah
- Event-driven architecture — Komunikasi antar microservices
- Live dashboard — Update data secara real-time ke frontend
Contoh Pub/Sub dengan Redis CLI
# Terminal 1: Subscribe ke channel (akan menunggu pesan)
SUBSCRIBE notifikasi:order
# Terminal 2: Publish pesan ke channel
PUBLISH notifikasi:order '{"order_id":123,"status":"dikirim"}'
# Output: (integer) 1 ← Jumlah subscriber yang menerima
PUBLISH notifikasi:order '{"order_id":456,"status":"selesai"}'
# Output: (integer) 1
# Subscribe ke pattern (wildcard)
PSUBSCRIBE notifikasi:*
# Akan menerima pesan dari semua channel "notifikasi:*"
Implementasi Pub/Sub dengan Node.js
const Redis = require('ioredis');
// Perlu 2 koneksi terpisah (Redis requirement)
// Koneksi subscribe TIDAK bisa digunakan untuk operasi lain
const publisher = new Redis({ host: 'localhost', port: 6379 });
const subscriber = new Redis({ host: 'localhost', port: 6379 });
// === SUBSCRIBER ===
async function setupSubscriber() {
// Subscribe ke channel
await subscriber.subscribe('notifikasi:order', 'notifikasi:chat');
subscriber.on('message', (channel, message) => {
console.log(`[${channel}] Pesan diterima:`, message);
// Parse JSON dan proses
try {
const data = JSON.parse(message);
console.log(`Order #${data.order_id}: ${data.status}`);
} catch (e) {
console.log('Raw message:', message);
}
});
}
// === PUBLISHER ===
async function publishNotification(orderId, status) {
const message = JSON.stringify({
order_id: orderId,
status: status,
timestamp: new Date().toISOString()
});
const subscriberCount = await publisher.publish(
'notifikasi:order',
message
);
console.log(`Pesan terkirim ke ${subscriberCount} subscriber`);
}
// Jalankan
setupSubscriber().then(() => {
// Kirim beberapa notifikasi
setTimeout(() => publishNotification(123, 'dikirim'), 1000);
setTimeout(() => publishNotification(123, 'selesai'), 2000);
setTimeout(() => publishNotification(456, 'dibatalkan'), 3000);
});
- Fire-and-forget — Pesan tidak disimpan. Jika subscriber offline, pesan hilang
- No acknowledgement — Tidak ada mekanisme konfirmasi penerimaan
- Untuk reliable messaging, gunakan Redis Streams (Redis 5.0+) atau Bull/BullMQ (job queue berbasis Redis)
5. Caching Patterns
Caching bukan hanya tentang "simpan di Redis, baca dari Redis". Ada beberapa pattern yang berbeda, masing-masing dengan trade-off tersendiri. Memilih pattern yang tepat sangat tergantung pada karakteristik data dan beban kerja aplikasi Anda.
Pattern 1: Cache-Aside (Lazy Loading)
Pattern paling umum. Aplikasi bertanggung jawab untuk mengelola cache secara manual: cek cache dulu, kalau miss baru query database dan simpan ke cache.
┌─────────┐ ┌─────────┐
│ Client │──── Request ────► │ App │
└─────────┘ └────┬────┘
│
1. Cek Redis │
▼
┌──────────┐
│ Redis │
│ Cache │
└────┬─────┘
│
2a. HIT: return cached ──► Client
2b. MISS:
│
▼
3. Query ┌──────────┐
────────►│ Database │
◄────────│(MySQL/PG)│
4. Save └──────────┘
to cache
const Redis = require('ioredis');
const redis = new Redis();
const db = require('./database'); // Database connection
// ==========================================
// CACHE-ASIDE PATTERN
// ==========================================
async function getProductById(productId) {
const cacheKey = `product:${productId}`;
// Langkah 1: Cek cache
const cached = await redis.get(cacheKey);
if (cached) {
console.log('✅ Cache HIT');
return JSON.parse(cached);
}
// Langkah 2: Cache MISS — query database
console.log('❌ Cache MISS — querying database');
const product = await db.query(
'SELECT * FROM products WHERE id = ?', [productId]
);
if (!product) return null;
// Langkah 3: Simpan ke cache dengan TTL 1 jam
await redis.setex(cacheKey, 3600, JSON.stringify(product));
return product;
}
// ==========================================
// CACHE INVALIDATION
// ==========================================
async function updateProduct(productId, newData) {
// Update database dulu
await db.query(
'UPDATE products SET name = ?, price = ? WHERE id = ?',
[newData.name, newData.price, productId]
);
// Hapus cache (bukan update!) — biar lazy reload
await redis.del(`product:${productId}`);
// Atau: update cache langsung (write-through ringan)
const updated = { id: productId, ...newData };
await redis.setex(`product:${productId}`, 3600, JSON.stringify(updated));
}
Pattern 2: Write-Through
Setiap kali data ditulis ke database, data juga sekaligus ditulis ke cache. Cache selalu konsisten dengan database.
// ==========================================
// WRITE-THROUGH PATTERN
// ==========================================
async function createProduct(productData) {
// Langkah 1: Tulis ke database
const result = await db.query(
'INSERT INTO products (name, price, stock) VALUES (?, ?, ?)',
[productData.name, productData.price, productData.stock]
);
const newProduct = {
id: result.insertId,
...productData,
created_at: new Date().toISOString()
};
// Langkah 2: Tulis ke cache SEKALIGUS
const cacheKey = `product:${newProduct.id}`;
await redis.setex(cacheKey, 3600, JSON.stringify(newProduct));
// Langkah 3: Tambahkan ke list produk terbaru
await redis.lpush('products:latest', JSON.stringify(newProduct));
await redis.ltrim('products:latest', 0, 49); // Simpan 50 terbaru
return newProduct;
}
// Kelebihan: Cache selalu up-to-date
// Kekurangan: Write latency lebih tinggi (harus tulis ke 2 tempat)
// Cocok untuk: Data yang sering dibaca dan jarang diubah
Pattern 3: Write-Behind (Write-Back)
// ==========================================
// WRITE-BEHIND PATTERN
// Data ditulis ke cache dulu, database di-update belakangan
// ==========================================
const writeQueue = [];
async function updatePageView(pageId) {
// Langkah 1: Tulis ke Redis (sangat cepat)
await redis.incr(`page:${pageId}:views`);
// Langkah 2: Tambahkan ke queue untuk update database nanti
writeQueue.push({
type: 'page_view',
pageId: pageId,
timestamp: Date.now()
});
}
// Background worker — flush ke database setiap 10 detik
setInterval(async () => {
if (writeQueue.length === 0) return;
const batch = writeQueue.splice(0, writeQueue.length);
console.log(`Flushing ${batch.length} writes ke database...`);
try {
// Batch update ke database
for (const item of batch) {
await db.query(
'UPDATE pages SET views = views + 1 WHERE id = ?',
[item.pageId]
);
}
} catch (err) {
console.error('Flush error:', err);
// Kembalikan ke queue untuk retry
writeQueue.unshift(...batch);
}
}, 10000);
// Cocok untuk: Counter, analytics, logging — data yang toleran kehilangan
// ⚠️ Risiko: Data hilang jika Redis crash sebelum flush
Perbandingan Caching Patterns
| Pattern | Read Speed | Write Speed | Konsistensi | Kompleksitas | Cocok untuk |
|---|---|---|---|---|---|
| Cache-Aside | 🟢 Cepat (jika hit) | 🟡 Normal | ⚠️ Bisa stale | Rendah | Umum, product catalog |
| Write-Through | 🟢 Selalu cepat | 🟡 Lebih lambat | ✅ Konsisten | Sedang | Data penting, profil user |
| Write-Behind | 🟢 Selalu cepat | 🟢 Sangat cepat | ⚠️ Delay | Tinggi | Counter, analytics, log |
6. TTL & Key Expiration
TTL (Time To Live) adalah mekanisme untuk mengatur berapa lama sebuah key akan hidup di Redis sebelum otomatis dihapus. Ini sangat penting untuk mencegah cache menjadi stale dan mengontrol penggunaan memori.
Operasi TTL
# Set dengan expiry
SET session:abc123 '{"user_id":1}' EX 3600 # 1 jam
SET otp:0812345 "938271" EX 300 # 5 menit
SET temp:upload:file1 "processing" EX 1800 # 30 menit
# Set expiry pada key yang sudah ada
EXPIRE session:abc123 7200 # Ubah ke 2 jam
EXPIREAT session:abc123 1687766400 # Expire pada timestamp tertentu
# Cek sisa TTL
TTL session:abc123 # 3599 (detik tersisa)
PTTL session:abc123 # 3599234 (milidetik tersisa)
# Hasil -1 = key ada tanpa expiry
# Hasil -2 = key tidak ditemukan
# Hapus expiry (buat persisten)
PERSIST session:abc123 # TTL dihapus, key hidup selamanya
# Set kalau belum ada (opsional)
SETNX lock:resource1 "worker-1" # Hanya set jika key belum ada
Strategi TTL untuk Berbagai Kasus
| Kasus Penggunaan | TTL yang Disarankan | Alasan |
|---|---|---|
| Session user | 30 menit — 24 jam | Auto-logout setelah idle |
| OTP / Token verifikasi | 3 — 5 menit | Keamanan — OTP harus cepat expired |
| Product cache | 1 — 6 jam | Update tidak perlu real-time |
| API rate limit counter | 1 — 60 detik (window) | Counter di-reset per window |
| Cache populer / trending | 5 — 15 menit | Data berubah cepat |
| Distributed lock | 10 — 30 detik | Cegah deadlock jika worker crash |
7. Session Store dengan Redis
Menyimpan session di Redis (bukan di memory server atau database) adalah praktik standar untuk aplikasi production. Keuntungannya: persistent (tidak hilang saat server restart), shared (bisa diakses dari banyak server), dan cepat (in-memory).
Implementasi Session Store dengan Express.js
// Instalasi:
// npm install express express-session connect-redis ioredis
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const Redis = require('ioredis');
const app = express();
// Koneksi Redis
const redisClient = new Redis({
host: 'localhost',
port: 6379,
password: 'rahasia123',
db: 1 // Gunakan db terpisah untuk session
});
// Konfigurasi session dengan Redis store
app.use(session({
store: new RedisStore({
client: redisClient,
prefix: 'sess:', // Prefix key di Redis
ttl: 86400 // Session TTL: 24 jam (detik)
}),
secret: 'rahasia-session-***@example.com'
});
req.session.userId = user.id;
req.session.role = user.role;
res.json({ message: 'Login berhasil', user: { id: user.id, nama: user.nama } });
} else {
res.status(401).json({ error: 'Email atau password salah' });
}
});
// Logout — hapus session
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Gagal logout' });
res.clearCookie('sessionId');
res.json({ message: 'Logout berhasil' });
});
});
app.listen(3000, () => console.log('Server jalan di port 3000'));
Monitoring Session di Redis
# Lihat semua session keys
KEYS sess:*
# Ambil data session
GET sess:abc123def456
# Output: {"cookie":{...},"userId":1,"role":"user"}
# Cek TTL session
TTL sess:abc123def456
# Output: 82345 (sisa detik)
# Hitung jumlah session aktif
DBSIZE # (jika db khusus session)
# Hapus session tertentu (force logout)
DEL sess:abc123def456
# Hapus semua session (force logout semua user)
FLUSHDB # ⚠️ Hati-hati — hapus semua data di db saat ini
8. Rate Limiting
Rate limiting adalah teknik untuk membatasi jumlah request yang boleh dilakukan oleh satu client dalam periode waktu tertentu. Ini sangat penting untuk melindungi API dari abuse, brute force, dan DDoS. Redis sangat cocok untuk rate limiting karena operasinya atomic dan cepat.
Algoritma: Fixed Window Counter
const Redis = require('ioredis');
const redis = new Redis();
// ==========================================
// RATE LIMITER: Fixed Window Counter
// ==========================================
async function rateLimitFixedWindow(identifier, maxRequests, windowSeconds) {
const key = `ratelimit:${identifier}:${Math.floor(Date.now() / 1000 / windowSeconds)}`;
// Atomic increment
const current = await redis.incr(key);
// Set expiry hanya pada pertama kali (current === 1)
if (current === 1) {
await redis.expire(key, windowSeconds);
}
const remaining = Math.max(0, maxRequests - current);
const ttl = await redis.ttl(key);
return {
allowed: current <= maxRequests,
current: current,
remaining: remaining,
resetIn: ttl,
limit: maxRequests
};
}
// Contoh penggunaan
async function handleRequest(clientIp) {
// Maksimal 100 request per 15 menit per IP
const result = await rateLimitFixedWindow(clientIp, 100, 900);
if (!result.allowed) {
return {
status: 429,
headers: {
'X-RateLimit-Limit': result.limit,
'X-RateLimit-Remaining': 0,
'Retry-After': result.resetIn
},
body: { error: 'Terlalu banyak request. Coba lagi nanti.' }
};
}
// Request diizinkan
return { status: 200, headers: { 'X-RateLimit-Remaining': result.remaining } };
}
Algoritma: Sliding Window (Lebih Akurat)
// ==========================================
// RATE LIMITER: Sliding Window dengan Sorted Set
// Lebih akurat — tidak ada "burst" di batas window
// ==========================================
async function rateLimitSlidingWindow(identifier, maxRequests, windowSeconds) {
const key = `ratelimit:sliding:${identifier}`;
const now = Date.now();
const windowStart = now - (windowSeconds * 1000);
// Gunang Redis pipeline untuk efisiensi
const pipeline = redis.pipeline();
// 1. Hapus request lama yang sudah di luar window
pipeline.zremrangebyscore(key, 0, windowStart);
// 2. Hitung request dalam window saat ini
pipeline.zcard(key);
// 3. Tambah request baru
pipeline.zadd(key, now, `${now}:${Math.random()}`);
// 4. Set expiry pada key
pipeline.expire(key, windowSeconds);
const results = await pipeline.exec();
const requestCount = results[1][1]; // Hasil zcard
const allowed = requestCount < maxRequests;
const remaining = Math.max(0, maxRequests - requestCount - 1);
// Hapus request yang baru ditambah jika tidak diizinkan
if (!allowed) {
await redis.zrem(key, `${now}:${Math.random()}`);
}
return { allowed, current: requestCount + (allowed ? 1 : 0), remaining };
}
// Contoh: API endpoint dengan rate limiting
const express = require('express');
const app = express();
app.use(async (req, res, next) => {
const clientIp = req.ip || req.connection.remoteAddress;
const result = await rateLimitSlidingWindow(clientIp, 100, 900);
res.set('X-RateLimit-Limit', '100');
res.set('X-RateLimit-Remaining', result.remaining);
if (!result.allowed) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
next();
});
app.get('/api/data', (req, res) => {
res.json({ data: 'success' });
});
app.listen(3000);
- Per-IP — Untuk API publik, limit per IP address
- Per-User — Untuk API terautentikasi, limit per user ID
- Per-Endpoint — Endpoint berat (export, search) perlu limit lebih ketat
- Tiered — User premium dapat limit lebih tinggi dari user gratis
- Selalu kirim header
X-RateLimit-RemainingdanRetry-Afteragar client tahu sisa kuota mereka
9. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Redis & Caching Strategy: