Redis #
Redis adalah in-memory data store yang bekerja dengan kecepatan luar biasa — operasi baca dan tulis selesai dalam hitungan mikrodetik karena seluruh data disimpan di RAM. Lebih dari sekadar cache sederhana, Redis mendukung berbagai struktur data seperti string, hash, list, set, dan sorted set yang masing-masing punya karakteristik dan use case berbeda. Di aplikasi modern, Redis sering muncul dalam beberapa peran sekaligus: cache untuk mengurangi beban database, penyimpan session, antrian tugas ringan, pub/sub broker, hingga mekanisme distributed lock. Di TypeScript, library ioredis menyediakan antarmuka yang matang dengan dukungan tipe yang baik, pipeline, cluster mode, dan Lua scripting.
Instalasi #
npm install ioredis
npm install --save-dev @types/ioredis
Untuk menjalankan Redis secara lokal via Docker:
docker run -d --name redis -p 6379:6379 redis:7-alpine
Koneksi ke Redis #
ioredis menangani reconnect secara otomatis — kamu tidak perlu menulis logika retry sendiri. Yang perlu diperhatikan adalah bagaimana mengelola instance client agar tidak membuat koneksi baru untuk setiap operasi.
import Redis from "ioredis";
// ANTI-PATTERN: membuat instance baru di setiap fungsi
async function ambilCache(key: string) {
const redis = new Redis(); // ✗ koneksi baru setiap panggilan
return redis.get(key);
}
// BENAR: singleton instance
class RedisClient {
private static instance: Redis;
static getInstance(): Redis {
if (!RedisClient.instance) {
RedisClient.instance = new Redis({
host: process.env.REDIS_HOST ?? "localhost",
port: Number(process.env.REDIS_PORT ?? 6379),
password: process.env.REDIS_PASSWORD,
db: 0, // database index (0–15)
maxRetriesPerRequest: 3,
retryStrategy(times) {
// retry dengan backoff eksponensial, maksimal 3 detik
return Math.min(times * 100, 3000);
},
enableOfflineQueue: true, // antrikan command saat koneksi putus
lazyConnect: false, // connect langsung saat instance dibuat
});
RedisClient.instance.on("connect", () => {
console.log("Redis terhubung");
});
RedisClient.instance.on("error", (err) => {
console.error("Redis error:", err.message);
});
RedisClient.instance.on("reconnecting", () => {
console.warn("Redis sedang mencoba reconnect...");
});
}
return RedisClient.instance;
}
static async disconnect(): Promise<void> {
if (RedisClient.instance) {
await RedisClient.instance.quit();
}
}
}
const redis = RedisClient.getInstance();
Koneksi ke Redis dengan URL (berguna untuk managed Redis seperti Upstash, Redis Cloud, atau Railway):
// koneksi via URL — format lengkap
const redis = new Redis("redis://:password@host:6379/0");
// Redis dengan TLS (Redis Cloud, Upstash)
const redisTLS = new Redis({
host: "my-redis.upstash.io",
port: 6379,
password: process.env.REDIS_PASSWORD,
tls: {}, // aktifkan TLS
});
Struktur Data Redis #
Redis bukan hanya key-value store biasa. Setiap struktur data yang disediakannya punya perintah atomik yang dirancang khusus untuk use case tertentu.
flowchart TD
A[Redis Data Structures] --> B[String\nnilai tunggal, counter, JSON]
A --> C[Hash\nobject / record]
A --> D[List\nantrian, stack, log]
A --> E[Set\ntag, unique member]
A --> F[Sorted Set\nleaderboard, rate limit]
A --> G[Stream\nevent log, message queue]String — Tipe Data Paling Dasar #
String di Redis bisa menyimpan teks, angka, atau data biner. Ukuran maksimalnya 512 MB per key.
// set dan get nilai sederhana
await redis.set("app:version", "1.0.0");
const versi = await redis.get("app:version"); // "1.0.0"
// set dengan TTL (Time To Live) — otomatis dihapus setelah N detik
await redis.set("otp:081234567890", "123456", "EX", 300); // kadaluarsa 5 menit
// setex — alternatif syntax dengan TTL
await redis.setex("session:abc123", 3600, JSON.stringify({ userId: 1 }));
// setnx — set hanya jika key belum ada (atomik)
const berhasil = await redis.setnx("lock:resource-1", "1");
// berhasil = 1 jika di-set, 0 jika sudah ada
// getset — ambil nilai lama sekaligus set nilai baru (atomik)
const nilaiLama = await redis.getset("counter:page-view", "0");
// increment / decrement — operasi atomik untuk counter
await redis.set("counter:like", "100");
await redis.incr("counter:like"); // 101
await redis.incrby("counter:like", 10); // 111
await redis.decr("counter:like"); // 110
// mset / mget — set atau get banyak key sekaligus (lebih efisien dari loop)
await redis.mset(
"user:1:nama", "Budi",
"user:1:email", "[email protected]",
"user:2:nama", "Sari"
);
const [namaBudi, emailBudi] = await redis.mget("user:1:nama", "user:1:email");
Hash — Menyimpan Object #
Hash adalah struktur key-value di dalam sebuah key — cocok untuk menyimpan objek tanpa perlu serialisasi JSON.
interface UserProfile {
nama: string;
email: string;
tier: string;
loginCount: string; // semua nilai hash disimpan sebagai string
}
const hashKey = "user:profile:1001";
// hset — set satu atau beberapa field
await redis.hset(hashKey, {
nama: "Budi Santoso",
email: "[email protected]",
tier: "premium",
loginCount: "0",
});
// hget — ambil satu field
const nama = await redis.hget(hashKey, "nama"); // "Budi Santoso"
// hmget — ambil beberapa field
const [email, tier] = await redis.hmget(hashKey, "email", "tier");
// hgetall — ambil semua field sebagai object
const profil = await redis.hgetall(hashKey) as UserProfile;
// hincrby — increment field numerik secara atomik
await redis.hincrby(hashKey, "loginCount", 1);
// hdel — hapus field tertentu
await redis.hdel(hashKey, "tier");
// hexists — cek apakah field ada
const ada = await redis.hexists(hashKey, "nama"); // 1 (ada) atau 0 (tidak ada)
// hkeys / hvals — ambil hanya key atau hanya value
const fields = await redis.hkeys(hashKey);
const values = await redis.hvals(hashKey);
Perbandingan Hash vs String JSON untuk menyimpan object:
// ANTI-PATTERN: simpan seluruh object sebagai JSON string
// untuk update satu field, kamu harus GET → parse → modifikasi → stringify → SET
await redis.set("user:1001", JSON.stringify({ nama: "Budi", loginCount: 0 }));
const raw = await redis.get("user:1001");
const obj = JSON.parse(raw!);
obj.loginCount++;
await redis.set("user:1001", JSON.stringify(obj)); // ✗ tidak atomik, rawan race condition
// BENAR: gunakan Hash untuk object yang field-nya sering diupdate secara independen
await redis.hset("user:1001", { nama: "Budi", loginCount: "0" });
await redis.hincrby("user:1001", "loginCount", 1); // ✓ atomik, tidak perlu GET dulu
List — Antrian dan Stack #
List adalah linked list yang mendukung operasi push/pop dari kedua ujungnya.
const antriKey = "antrian:email";
// rpush — tambah ke ujung kanan (end of list)
await redis.rpush(antriKey, JSON.stringify({ to: "[email protected]", subject: "Selamat Datang" }));
await redis.rpush(antriKey, JSON.stringify({ to: "[email protected]", subject: "Reset Password" }));
// lpop — ambil dari ujung kiri (FIFO queue)
const tugas = await redis.lpop(antriKey);
if (tugas) {
const email = JSON.parse(tugas);
console.log("Kirim email ke:", email.to);
}
// blpop — blocking lpop — tunggu sampai ada elemen (untuk worker)
const item = await redis.blpop(antriKey, 5); // timeout 5 detik
// item = [namaKey, nilai] atau null jika timeout
// lrange — ambil elemen dalam rentang indeks
const semuaTugas = await redis.lrange(antriKey, 0, -1); // 0 = pertama, -1 = terakhir
// llen — panjang list
const jumlah = await redis.llen(antriKey);
// untuk stack (LIFO), gunakan lpush + lpop
await redis.lpush("stack:undo", JSON.stringify({ action: "delete", id: 5 }));
const aksiTerakhir = await redis.lpop("stack:undo");
Set — Kumpulan Nilai Unik #
Set menjamin tidak ada duplikat. Cocok untuk menyimpan tag, daftar ID yang sudah diproses, atau membership.
const tagKey = "produk:1001:tags";
// sadd — tambah anggota (duplikat diabaikan)
await redis.sadd(tagKey, "laptop", "gaming", "premium");
await redis.sadd(tagKey, "laptop"); // tidak menambah duplikat
// smembers — ambil semua anggota
const tags = await redis.smembers(tagKey);
// sismember — cek apakah nilai ada di set
const adaTag = await redis.sismember(tagKey, "gaming"); // 1 atau 0
// scard — jumlah anggota
const jumlahTag = await redis.scard(tagKey);
// srem — hapus anggota
await redis.srem(tagKey, "premium");
// operasi set: union, intersection, difference
const tagsA = "produk:1001:tags";
const tagsB = "produk:1002:tags";
await redis.sadd(tagsA, "laptop", "gaming", "asus");
await redis.sadd(tagsB, "laptop", "office", "asus");
const union = await redis.sunion(tagsA, tagsB); // gabungan
const intersect = await redis.sinter(tagsA, tagsB); // irisan
const diff = await redis.sdiff(tagsA, tagsB); // selisih (ada di A, tidak di B)
Sorted Set — Set dengan Skor #
Sorted set seperti set biasa, tapi setiap anggota punya skor numerik. Anggota selalu terurut berdasarkan skor secara otomatis.
const leaderboardKey = "game:leaderboard";
// zadd — tambah anggota dengan skor
await redis.zadd(leaderboardKey, 9800, "player:budi");
await redis.zadd(leaderboardKey, 12500, "player:sari");
await redis.zadd(leaderboardKey, 7300, "player:andi");
// zincrby — tambah skor secara atomik
await redis.zincrby(leaderboardKey, 500, "player:budi"); // skor jadi 10300
// zrange — ambil anggota dari skor terendah ke tertinggi
const semuaPemain = await redis.zrange(leaderboardKey, 0, -1, "WITHSCORES");
// zrevrange — dari skor tertinggi ke terendah (untuk leaderboard)
const top10 = await redis.zrevrange(leaderboardKey, 0, 9, "WITHSCORES");
// zrank / zrevrank — posisi anggota (0-indexed)
const posisi = await redis.zrevrank(leaderboardKey, "player:budi");
// posisi = 1 jika dia urutan kedua
// zscore — ambil skor satu anggota
const skor = await redis.zscore(leaderboardKey, "player:sari");
// zrangebyscore — filter berdasarkan rentang skor
const pemainAktif = await redis.zrangebyscore(leaderboardKey, 5000, "+inf");
Pola Caching #
Caching adalah use case paling umum Redis. Berikut dua pola utama yang perlu kamu pahami.
Cache-Aside (Lazy Loading) #
Pola paling umum — aplikasi mengelola cache secara manual. Data diambil dari cache dulu; jika tidak ada (cache miss), baru ambil dari database dan simpan ke cache.
sequenceDiagram
participant App
participant Redis
participant DB
App->>Redis: GET produk:1001
alt Cache Hit
Redis-->>App: data produk
else Cache Miss
Redis-->>App: null
App->>DB: SELECT * FROM produk WHERE id = 1001
DB-->>App: data produk
App->>Redis: SET produk:1001 (TTL 10 menit)
Redis-->>App: OK
endasync function ambilProduk(id: string): Promise<Produk> {
const cacheKey = `produk:${id}`;
// 1. coba dari cache dulu
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached) as Produk;
}
// 2. cache miss — ambil dari database
const produk = await db.query<Produk>("SELECT * FROM produk WHERE id = $1", [id]);
if (!produk) {
throw new Error(`Produk ${id} tidak ditemukan`);
}
// 3. simpan ke cache dengan TTL
await redis.set(cacheKey, JSON.stringify(produk), "EX", 600); // 10 menit
return produk;
}
// invalidasi cache saat data berubah
async function updateProduk(id: string, data: Partial<Produk>): Promise<void> {
await db.query("UPDATE produk SET ... WHERE id = $1", [id]);
// hapus cache agar request berikutnya mengambil data terbaru
await redis.del(`produk:${id}`);
}
Write-Through #
Data selalu ditulis ke cache sekaligus ke database secara bersamaan. Cache selalu konsisten dengan database.
async function simpanProdukWriteThrough(produk: Produk): Promise<void> {
// tulis ke database dan cache secara bersamaan
await Promise.all([
db.query("INSERT INTO produk ... VALUES ...", [produk]),
redis.set(`produk:${produk.id}`, JSON.stringify(produk), "EX", 600),
]);
}
Generic Cache Wrapper #
Untuk menghindari duplikasi logika cache-aside di seluruh kodebase, buat wrapper yang reusable:
async function withCache<T>(
key: string,
ttlDetik: number,
fetchFn: () => Promise<T>
): Promise<T> {
// coba dari cache
const cached = await redis.get(key);
if (cached !== null) {
return JSON.parse(cached) as T;
}
// cache miss — jalankan fungsi asli
const data = await fetchFn();
// simpan ke cache (jangan simpan null/undefined)
if (data !== null && data !== undefined) {
await redis.set(key, JSON.stringify(data), "EX", ttlDetik);
}
return data;
}
// penggunaan
const produk = await withCache(
`produk:${id}`,
600,
() => db.findById("produk", id)
);
const daftarKategori = await withCache(
"kategori:semua",
3600, // cache 1 jam karena jarang berubah
() => db.query("SELECT * FROM kategori ORDER BY nama")
);
Session Management #
Redis sangat cocok untuk menyimpan session karena mendukung TTL per key — session otomatis kadaluarsa tanpa perlu cleanup job.
import { randomUUID } from "crypto";
interface SessionData {
userId: string;
email: string;
role: string;
loginAt: string;
lastActiveAt: string;
}
class SessionStore {
private readonly prefix = "session:";
private readonly ttl = 86400; // 24 jam dalam detik
private key(sessionId: string): string {
return `${this.prefix}${sessionId}`;
}
async buat(data: Omit<SessionData, "loginAt" | "lastActiveAt">): Promise<string> {
const sessionId = randomUUID();
const sessionData: SessionData = {
...data,
loginAt: new Date().toISOString(),
lastActiveAt: new Date().toISOString(),
};
await redis.set(this.key(sessionId), JSON.stringify(sessionData), "EX", this.ttl);
return sessionId;
}
async ambil(sessionId: string): Promise<SessionData | null> {
const raw = await redis.get(this.key(sessionId));
if (!raw) return null;
return JSON.parse(raw) as SessionData;
}
async perbarui(sessionId: string): Promise<boolean> {
const sesi = await this.ambil(sessionId);
if (!sesi) return false;
sesi.lastActiveAt = new Date().toISOString();
// perbarui data dan reset TTL bersamaan
await redis.set(this.key(sessionId), JSON.stringify(sesi), "EX", this.ttl);
return true;
}
async hapus(sessionId: string): Promise<void> {
await redis.del(this.key(sessionId));
}
async hapusSemuaMilikUser(userId: string): Promise<void> {
// scan semua session key — gunakan SCAN bukan KEYS di production
const stream = redis.scanStream({
match: `${this.prefix}*`,
count: 100,
});
const keysToDelete: string[] = [];
for await (const keys of stream) {
for (const key of keys as string[]) {
const raw = await redis.get(key);
if (raw) {
const sesi = JSON.parse(raw) as SessionData;
if (sesi.userId === userId) {
keysToDelete.push(key);
}
}
}
}
if (keysToDelete.length > 0) {
await redis.del(...keysToDelete);
}
}
}
const sessionStore = new SessionStore();
Jangan gunakan perintahKEYS *di production — perintah ini memblokir Redis hingga seluruh keyspace selesai dipindai. GunakanSCANdenganscanStream()dari ioredis yang bersifat non-blocking dan iteratif.
Rate Limiting #
Rate limiting mencegah penyalahgunaan API dengan membatasi jumlah request dalam rentang waktu tertentu. Redis sangat cocok untuk ini karena operasinya atomik dan TTL-nya bisa dimanfaatkan langsung.
Fixed Window Rate Limiter #
async function cekRateLimit(
identifier: string, // bisa IP, userId, atau API key
limitPerMenit: number
): Promise<{ diizinkan: boolean; sisaRequest: number; resetDalam: number }> {
const sekarang = Math.floor(Date.now() / 1000);
const window = Math.floor(sekarang / 60); // window per menit
const key = `rate:${identifier}:${window}`;
// increment dan set TTL dalam satu pipeline atomik
const pipeline = redis.pipeline();
pipeline.incr(key);
pipeline.expire(key, 60);
const hasil = await pipeline.exec();
const jumlahRequest = (hasil?.[0]?.[1] as number) ?? 0;
const diizinkan = jumlahRequest <= limitPerMenit;
const sisaRequest = Math.max(0, limitPerMenit - jumlahRequest);
const resetDalam = 60 - (sekarang % 60);
return { diizinkan, sisaRequest, resetDalam };
}
// penggunaan di middleware Express
async function rateLimitMiddleware(
req: any,
res: any,
next: any
): Promise<void> {
const ip = req.ip;
const { diizinkan, sisaRequest, resetDalam } = await cekRateLimit(ip, 100);
res.setHeader("X-RateLimit-Remaining", sisaRequest);
res.setHeader("X-RateLimit-Reset", resetDalam);
if (!diizinkan) {
res.status(429).json({
error: "Terlalu banyak request",
cobaLagiDalam: `${resetDalam} detik`,
});
return;
}
next();
}
Sliding Window Rate Limiter dengan Sorted Set #
Fixed window memiliki kelemahan di ujung window — pengguna bisa kirim 100 request di detik ke-59, lalu 100 lagi di detik ke-61 (window baru). Sliding window lebih akurat:
async function cekRateLimitSliding(
identifier: string,
limitPerMenit: number
): Promise<{ diizinkan: boolean; jumlahRequest: number }> {
const key = `rate:sliding:${identifier}`;
const sekarang = Date.now();
const window = 60 * 1000; // 1 menit dalam milidetik
const batasWaktu = sekarang - window;
const pipeline = redis.pipeline();
// hapus entry yang sudah di luar window
pipeline.zremrangebyscore(key, "-inf", batasWaktu);
// tambah request saat ini
pipeline.zadd(key, sekarang, `${sekarang}-${Math.random()}`);
// hitung total request dalam window
pipeline.zcard(key);
// set TTL agar key bersih sendiri
pipeline.expire(key, 60);
const hasil = await pipeline.exec();
const jumlahRequest = (hasil?.[2]?.[1] as number) ?? 0;
return {
diizinkan: jumlahRequest <= limitPerMenit,
jumlahRequest,
};
}
Pub/Sub — Komunikasi Antar Proses #
Redis Pub/Sub memungkinkan satu proses mempublikasikan pesan dan proses lain menerima pesan tersebut secara real-time. Berguna untuk notifikasi, invalidasi cache terdistribusi, atau komunikasi antar microservice.
// PENTING: subscriber memerlukan koneksi Redis tersendiri
// karena koneksi yang sedang dalam mode subscribe tidak bisa digunakan untuk perintah lain
const publisher = new Redis({ host: "localhost", port: 6379 });
const subscriber = new Redis({ host: "localhost", port: 6379 });
// subscriber — dengarkan channel
await subscriber.subscribe("notifikasi:pesanan", "notifikasi:pembayaran");
subscriber.on("message", (channel: string, message: string) => {
const data = JSON.parse(message);
switch (channel) {
case "notifikasi:pesanan":
console.log("Pesanan baru:", data);
// kirim email, push notification, dll
break;
case "notifikasi:pembayaran":
console.log("Pembayaran diterima:", data);
break;
}
});
// publisher — kirim pesan ke channel
async function publikasikanPesananBaru(pesanan: {
id: string;
userId: string;
total: number;
}): Promise<void> {
await publisher.publish(
"notifikasi:pesanan",
JSON.stringify({ ...pesanan, timestamp: new Date().toISOString() })
);
}
// pattern subscribe — subscribe ke beberapa channel dengan wildcard
await subscriber.psubscribe("notifikasi:*");
subscriber.on("pmessage", (pattern: string, channel: string, message: string) => {
console.log(`Pesan dari ${channel} (pattern ${pattern}):`, message);
});
Redis Pub/Sub bersifat fire-and-forget — pesan tidak disimpan. Jika subscriber sedang offline saat pesan dikirim, pesan tersebut hilang. Untuk keandalan yang lebih tinggi, gunakan Redis Streams atau message broker seperti RabbitMQ dan Kafka.
Distributed Lock #
Saat beberapa instance aplikasi berjalan bersamaan, distributed lock memastikan hanya satu instance yang mengerjakan tugas tertentu di satu waktu — misalnya proses cron job atau pembayaran.
class RedisLock {
private readonly lockPrefix = "lock:";
private readonly defaultTTL = 30; // detik
async kunci(
resource: string,
ttlDetik: number = this.defaultTTL
): Promise<string | null> {
const lockKey = `${this.lockPrefix}${resource}`;
const lockValue = randomUUID(); // nilai unik per pemilik lock
// SET NX EX — atomik: set hanya jika belum ada, dengan TTL
const hasil = await redis.set(lockKey, lockValue, "EX", ttlDetik, "NX");
return hasil === "OK" ? lockValue : null; // null = gagal dapat lock
}
async bebaskan(resource: string, lockValue: string): Promise<boolean> {
const lockKey = `${this.lockPrefix}${resource}`;
// Lua script untuk atomik: hanya hapus jika value cocok (milik kita)
// tanpa ini, ada risiko menghapus lock milik proses lain
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
const hasil = await redis.eval(script, 1, lockKey, lockValue);
return hasil === 1;
}
async withLock<T>(
resource: string,
ttlDetik: number,
fn: () => Promise<T>
): Promise<T> {
const lockValue = await this.kunci(resource, ttlDetik);
if (!lockValue) {
throw new Error(`Gagal mendapatkan lock untuk resource: ${resource}`);
}
try {
return await fn();
} finally {
await this.bebaskan(resource, lockValue);
}
}
}
const lock = new RedisLock();
// penggunaan — hanya satu proses yang bisa jalankan ini bersamaan
async function prosesTagihan(tagihanId: string): Promise<void> {
await lock.withLock(`tagihan:${tagihanId}`, 30, async () => {
// kode di sini hanya dijalankan oleh satu instance
const tagihan = await db.findById("tagihan", tagihanId);
if (tagihan.status === "selesai") return;
await prosesPaymentGateway(tagihan);
await db.update("tagihan", tagihanId, { status: "selesai" });
});
}
Pipeline — Mengirim Banyak Perintah Sekaligus #
Setiap perintah Redis memiliki overhead round-trip jaringan. Pipeline memungkinkan kamu mengirim banyak perintah dalam satu request, lalu menerima semua responsnya sekaligus.
// ANTI-PATTERN: perintah satu per satu — N round-trip jaringan
async function simpanUserDataSatuSatu(userId: string, data: any): Promise<void> {
await redis.hset(`user:${userId}`, data); // round-trip 1
await redis.expire(`user:${userId}`, 3600); // round-trip 2
await redis.sadd("users:aktif", userId); // round-trip 3
await redis.incr("counter:users:total"); // round-trip 4
}
// BENAR: pipeline — satu round-trip untuk semua perintah
async function simpanUserDataPipeline(userId: string, data: any): Promise<void> {
const pipeline = redis.pipeline();
pipeline.hset(`user:${userId}`, data);
pipeline.expire(`user:${userId}`, 3600);
pipeline.sadd("users:aktif", userId);
pipeline.incr("counter:users:total");
// semua perintah dikirim dan dieksekusi bersamaan
const hasil = await pipeline.exec();
// cek error per perintah
hasil?.forEach(([err, result], index) => {
if (err) {
console.error(`Perintah ke-${index + 1} gagal:`, err);
}
});
}
Kapan Menggunakan Redis #
Gunakan Redis untuk:
✓ Cache hasil query database yang mahal atau sering diakses
✓ Menyimpan session pengguna dengan TTL otomatis
✓ Rate limiting request per IP atau per user
✓ Leaderboard dan ranking real-time dengan sorted set
✓ Antrian tugas ringan (email, notifikasi) dengan list
✓ Distributed lock untuk koordinasi antar instance
✓ Pub/sub untuk notifikasi real-time antar service
✓ Counter dan statistik real-time (page view, like count)
Jangan andalkan Redis untuk:
✗ Data yang tidak boleh hilang — Redis bisa kehilangan data saat crash tanpa konfigurasi persistence
✗ Data lebih besar dari RAM server — Redis bekerja di memori
✗ Query kompleks seperti join antar "tabel" — gunakan database relasional
✗ Satu-satunya penyimpan session jika tidak ada backup — selalu punya fallback
Ringkasan #
- Singleton instance dengan konfigurasi
retryStrategy— jangan buat koneksi Redis baru untuk setiap operasi; biarkan ioredis menangani reconnect otomatis.- Pilih struktur data yang tepat — String untuk nilai tunggal dan counter; Hash untuk object yang field-nya diupdate independen; List untuk antrian FIFO; Set untuk keanggotaan unik; Sorted Set untuk ranking dan sliding window.
- Cache-aside adalah pola default — coba cache dulu, fetch dari DB jika miss, simpan ke cache dengan TTL; selalu invalidasi cache saat data berubah.
- TTL adalah fitur, bukan afterthought — hampir semua key di Redis harus punya TTL untuk mencegah akumulasi data stale yang menghabiskan memori.
- Pipeline untuk operasi berulang — gunakan
redis.pipeline()saat mengirim beberapa perintah bersamaan untuk menghindari overhead round-trip jaringan yang berulang.- Subscriber perlu koneksi tersendiri — koneksi Redis yang sedang dalam mode subscribe tidak bisa digunakan untuk perintah lain; selalu buat instance Redis terpisah untuk publisher dan subscriber.
- Distributed lock dengan Lua script — gunakan script atomik untuk memastikan hanya pemilik lock yang bisa membebaskannya; hindari race condition antara
GETdanDEL.- Gunakan
SCANbukanKEYS—KEYS *memblokir seluruh Redis saat keyspace besar; gunakanscanStream()dari ioredis yang iteratif dan non-blocking.