Database

Redis & Caching Strategy untuk Developer

TOKEN

Panduan lengkap Redis dan caching — data types, pub/sub messaging, caching patterns (cache-aside, write-through), TTL, session store, dan rate limiting untuk aplikasi production

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-MemorySeluruh data disimpan di RAM — akses data ~100.000x lebih cepat dari disk SSD
Single-ThreadedTidak ada overhead locking/context switching — memanfaatkan event loop efisien
I/O MultiplexingMenggunakan epoll/kqueue untuk menangani ribuan koneksi secara paralel
Data Structure OptimizedStruktur data dioptimalkan secara khusus — bukan general-purpose seperti HashMap
Protocol SederhanaRESP (Redis Serialization Protocol) sangat ringan dan cepat di-parse

Redis vs Alternatif Lain

Aspek Redis Memcached Varnish
Data TypesBeragam (string, list, set, hash, sorted set)Hanya stringHTTP objects
Persistence✅ RDB + AOF✅ Disk-backed
Pub/Sub
Clustering✅ Built-inClient-side
Scripting✅ LuaVCL
Cocok untukCache, session, queue, analyticsSimple key-value cacheHTTP cache/reverse proxy
Diagram: Posisi Redis dalam Arsitektur Aplikasi
┌─────────────────────────────────────────────────────────────────┐
│                    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)

Bash
# 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

Bash
# 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

JavaScript — Node.js dengan ioredis
// 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

Redis CLI
# 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

Redis CLI
# 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

Redis CLI
# 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

Redis CLI
# 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.

💡 Kapan Menggunakan Pub/Sub?
  • 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

Redis CLI — Publisher
# 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

JavaScript — pub-sub.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);
});
⚠️ Limitasi Redis Pub/Sub
  • 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.

Diagram: Cache-Aside Pattern
┌─────────┐                           ┌─────────┐
│  Client  │──── Request ────►        │  App    │
└─────────┘                           └────┬────┘
                                           │
                              1. Cek Redis │
                                           ▼
                                    ┌──────────┐
                                    │  Redis   │
                                    │  Cache   │
                                    └────┬─────┘
                                         │
                              2a. HIT: return cached ──► Client
                              2b. MISS:
                                         │
                                         ▼
                              3. Query ┌──────────┐
                              ────────►│ Database │
                              ◄────────│(MySQL/PG)│
                              4. Save  └──────────┘
                              to cache
JavaScript — Cache-Aside
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.

JavaScript — Write-Through
// ==========================================
// 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)

JavaScript — Write-Behind
// ==========================================
// 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 staleRendahUmum, product catalog
Write-Through🟢 Selalu cepat🟡 Lebih lambat✅ KonsistenSedangData penting, profil user
Write-Behind🟢 Selalu cepat🟢 Sangat cepat⚠️ DelayTinggiCounter, 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

Redis CLI
# 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 user30 menit — 24 jamAuto-logout setelah idle
OTP / Token verifikasi3 — 5 menitKeamanan — OTP harus cepat expired
Product cache1 — 6 jamUpdate tidak perlu real-time
API rate limit counter1 — 60 detik (window)Counter di-reset per window
Cache populer / trending5 — 15 menitData berubah cepat
Distributed lock10 — 30 detikCegah 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

JavaScript — server.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

Redis CLI
# 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

JavaScript — Rate Limiter
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)

JavaScript — Sliding Window
// ==========================================
// 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);
💡 Tips Rate Limiting
  • 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-Remaining dan Retry-After agar 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:

Pertanyaan 1: Mengapa Redis sangat cepat dalam memproses data?

a) Karena menggunakan GPU untuk komputasi
b) Karena seluruh data disimpan di RAM (in-memory) dan menggunakan struktur data yang dioptimalkan
c) Karena menggunakan multi-threading paralel
d) Karena mengompresi semua data sebelum penyimpanan

Pertanyaan 2: Dalam pattern Cache-Aside, apa yang terjadi ketika terjadi cache miss?

a) Client menerima error 404
b) Aplikasi mengembalikan data dari database dan menyimpannya ke cache untuk request berikutnya
c) Cache otomatis mengisi dirinya sendiri dari database
d) Request dialihkan ke server lain

Pertanyaan 3: Apa fungsi dari TTL (Time To Live) di Redis?

a) Mengenkripsi data di Redis
b) Mengatur prioritas akses data
c) Mengatur berapa lama key akan hidup sebelum otomatis dihapus
d) Mengompresi data untuk menghemat memori

Pertanyaan 4: Mengapa session lebih baik disimpan di Redis daripada di memory server atau database?

a) Karena Redis lebih murah dari database
b) Karena Redis tidak memerlukan koneksi jaringan
c) Karena session di Redis shared antar server, cepat diakses, dan tidak hilang saat restart
d) Karena Redis secara otomatis mengenkripsi session

Pertanyaan 5: Apa kelebihan algoritma Sliding Window dibanding Fixed Window untuk rate limiting?

a) Sliding Window menggunakan lebih sedikit memori
b) Sliding Window lebih akurat dan tidak mengalami masalah "burst" di batas window
c) Sliding Window tidak memerlukan Redis
d) Sliding Window mendukung unlimited requests