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 |
flowchart TB
Client["📱 Client"] -->|"Request"| App["🖥️ App Server"]
App -->|"Check Cache"| Redis["⚡ Redis\n(In-Memory Cache)"]
Redis -->|"Cache Hit ✅"| App
Redis -->|"Cache Miss ❌"| DB["("#quot;🗄️ Database\n(PostgreSQL/MySQL")#quot;)"]
DB -->|"Data"| Redis
Redis -->|"Store + Return"| App
App -->|"Response"| Client
style Client fill:#2a0f1e,stroke:#f472b6
style App fill:#0f1d3a,stroke:#60a5fa,stroke-width:2px
style Redis fill:#2a1f0a,stroke:#f59e0b,stroke-width:2px
style DB fill:#0f2a1f,stroke:#3ecf8e,stroke-width:1.5px
2. Instalasi & Setup Redis
Docker (Rekomendasi untuk Development)
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: