Redis #
Database hampir selalu menjadi bottleneck pertama ketika traffic meningkat. Query yang tadinya cepat mulai terasa lambat ketika dieksekusi ratusan kali per detik, dan skema yang tampak efisien mulai berjuang menghadapi volume data yang terus tumbuh. Redis hadir sebagai lapisan di antara aplikasi dan database — sebuah in-memory data store yang mampu melayani jutaan operasi per detik dengan latensi di bawah satu milidetik. Tapi Redis bukan sekadar cache sederhana. Ia adalah struktur data server yang mendukung String, Hash, List, Set, Sorted Set, dan banyak lagi — masing-masing dengan semantik operasi yang kaya dan dapat dimanfaatkan untuk memecahkan masalah yang jauh melampaui caching sederhana, mulai dari rate limiting, session management, distributed lock, leaderboard real-time, hingga message queue ringan.
Memilih Client Library #
Di ekosistem Java, ada dua client library Redis yang paling banyak digunakan: Jedis dan Lettuce. Keduanya matang dan teruji di production, tapi punya karakteristik yang berbeda.
| Jedis | Lettuce | |
|---|---|---|
| Model I/O | Synchronous (blocking) | Asynchronous & Reactive |
| Thread safety | Tidak — butuh connection pool | Ya — satu connection bisa dipakai banyak thread |
| Spring Boot default | Tidak (dulu ya, sekarang Lettuce) | Ya (sejak Spring Boot 2) |
| Kemudahan | Sangat mudah, API intuitif | Sedikit lebih kompleks |
| Throughput tinggi | Butuh pool besar | Efisien dengan sedikit connection |
| Redis Cluster | Didukung | Didukung, lebih matang |
Untuk kebanyakan proyek, Jedis dengan connection pool adalah pilihan paling mudah dimulai. Lettuce lebih cocok jika kamu butuh reactive programming atau integrasi dengan Spring WebFlux.
Setup Dependencies #
Jedis #
<!-- Maven -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.3</version>
</dependency>
// Gradle
implementation 'redis.clients:jedis:5.1.3'
Lettuce #
<!-- Maven -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.3.2.RELEASE</version>
</dependency>
// Gradle
implementation 'io.lettuce:lettuce-core:6.3.2.RELEASE'
Untuk menjalankan Redis secara lokal:
# Docker — cara tercepat
docker run -d \
--name redis \
-p 6379:6379 \
redis:7.2-alpine
# Dengan password
docker run -d \
--name redis \
-p 6379:6379 \
redis:7.2-alpine \
redis-server --requirepass "rahasia123"
# Akses Redis CLI
docker exec -it redis redis-cli
Koneksi dan Connection Pool #
Jedis dengan JedisPool #
Jedis tidak thread-safe, jadi kamu butuh connection pool. Setiap thread mengambil connection dari pool, menggunakannya, lalu mengembalikan ke pool.
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisConnectionFactory {
// ✗ ANTI-PATTERN: buat Jedis baru setiap kali dibutuhkan
// Setiap new Jedis() membuka TCP connection baru — sangat mahal
public static Jedis createInsecure() {
return new Jedis("localhost", 6379); // jangan pakai ini di production
}
// ✓ BENAR: gunakan JedisPool — satu pool, banyak connection yang di-reuse
public static JedisPool createPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(20); // maks 20 connection aktif
config.setMaxIdle(10); // maks 10 connection idle
config.setMinIdle(2); // pertahankan minimal 2 connection
config.setTestOnBorrow(true); // validasi connection sebelum dipakai
config.setTestOnReturn(true); // validasi connection saat dikembalikan
config.setBlockWhenExhausted(true); // tunggu jika pool penuh (jangan throw langsung)
config.setMaxWait(java.time.Duration.ofSeconds(5)); // tunggu maks 5 detik
// Tanpa password
return new JedisPool(config, "localhost", 6379);
// Dengan password
// return new JedisPool(config, "localhost", 6379, 2000, "rahasia123");
}
// Cara pemakaian yang benar — try-with-resources mengembalikan ke pool otomatis
public static void contohPemakaian(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
jedis.set("kunci", "nilai");
String nilai = jedis.get("kunci");
System.out.println("Nilai: " + nilai);
} // jedis otomatis dikembalikan ke pool di sini
}
}
Lettuce — Single Connection untuk Semua Thread #
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
public class LettuceConnectionFactory {
public static RedisClient createClient() {
RedisURI redisUri = RedisURI.builder()
.withHost("localhost")
.withPort(6379)
// .withPassword("rahasia123".toCharArray())
.withDatabase(0)
.withTimeout(java.time.Duration.ofSeconds(5))
.build();
return RedisClient.create(redisUri);
}
public static void contohPemakaian(RedisClient client) {
// Satu StatefulRedisConnection bisa dipakai dari banyak thread
try (StatefulRedisConnection<String, String> connection = client.connect()) {
RedisCommands<String, String> commands = connection.sync();
commands.set("kunci", "nilai");
String nilai = commands.get("kunci");
System.out.println("Nilai: " + nilai);
}
// Tutup client saat aplikasi shutdown
// client.shutdown();
}
}
Struktur Data String #
String adalah tipe data paling dasar di Redis. Walaupun namanya String, ia bisa menyimpan teks, angka, atau binary data (sampai 512 MB).
public class RedisStringDemo {
private final JedisPool pool;
public RedisStringDemo(JedisPool pool) {
this.pool = pool;
}
public void demo() {
try (Jedis jedis = pool.getResource()) {
// SET dan GET dasar
jedis.set("nama", "Budi");
String nama = jedis.get("nama");
System.out.println("nama: " + nama); // "Budi"
// SET dengan TTL (expire) — kunci otomatis dihapus setelah N detik
jedis.setex("session:abc123", 3600, "{\"userId\":42,\"role\":\"admin\"}");
// atau menggunakan SET dengan opsi EX
jedis.set("token:xyz", "bearer-token", redis.clients.jedis.params.SetParams.setParams().ex(1800));
// Cek TTL yang tersisa
long ttl = jedis.ttl("session:abc123");
System.out.println("TTL tersisa: " + ttl + " detik");
// INCR / DECR — atomic increment untuk counter
jedis.set("counter:views", "0");
jedis.incr("counter:views"); // 1
jedis.incr("counter:views"); // 2
jedis.incrBy("counter:views", 10); // 12
System.out.println("Views: " + jedis.get("counter:views")); // "12"
// SETNX — set hanya jika kunci belum ada (untuk distributed lock sederhana)
boolean diset = jedis.setnx("lock:resource", "1") == 1;
System.out.println("Lock berhasil: " + diset);
// GETSET — ambil nilai lama, set nilai baru secara atomic
String nilaiLama = jedis.getSet("nama", "Andi");
System.out.println("Nilai lama: " + nilaiLama + ", nilai baru: " + jedis.get("nama"));
// MGET / MSET — operasi batch
jedis.mset("kota", "Jakarta", "negara", "Indonesia", "benua", "Asia");
java.util.List<String> hasil = jedis.mget("kota", "negara", "benua");
System.out.println("Batch: " + hasil); // [Jakarta, Indonesia, Asia]
// EXISTS dan DELETE
boolean ada = jedis.exists("nama");
jedis.del("nama", "kota"); // hapus beberapa kunci sekaligus
}
}
}
Struktur Data Hash #
Hash menyimpan pasangan field-value dalam satu kunci. Sangat cocok untuk merepresentasikan objek — lebih efisien dari menyimpan JSON string karena field bisa diakses atau diperbarui secara individual.
public class RedisHashDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// HSET — set satu atau beberapa field
jedis.hset("user:1001", "nama", "Sari");
jedis.hset("user:1001", "email", "[email protected]");
jedis.hset("user:1001", "umur", "28");
// Atau set banyak field sekaligus
jedis.hset("user:1002", java.util.Map.of(
"nama", "Budi",
"email", "[email protected]",
"umur", "35",
"kota", "Bandung"
));
// HGET — ambil satu field
String email = jedis.hget("user:1001", "email");
System.out.println("Email: " + email);
// HGETALL — ambil semua field dan nilai
java.util.Map<String, String> user = jedis.hgetAll("user:1001");
System.out.println("User: " + user);
// HMGET — ambil beberapa field sekaligus
java.util.List<String> fields = jedis.hmget("user:1002", "nama", "kota");
System.out.println("Nama dan kota: " + fields);
// HINCRBY — increment field numerik secara atomic
jedis.hincrBy("user:1001", "umur", 1); // ulang tahun!
// HEXISTS — cek apakah field ada
boolean adaKota = jedis.hexists("user:1001", "kota");
System.out.println("Ada field kota: " + adaKota); // false
// HDEL — hapus field tertentu
jedis.hdel("user:1002", "kota");
// HKEYS / HVALS / HLEN
java.util.Set<String> keys = jedis.hkeys("user:1002");
java.util.List<String> values = jedis.hvals("user:1002");
long jumlahField = jedis.hlen("user:1002");
System.out.println("Fields: " + keys + " | Jumlah: " + jumlahField);
}
}
}
Struktur Data List #
List adalah linked list yang mendukung operasi push dan pop dari kedua ujung. Cocok untuk queue, stack, activity feed, dan log terbatas.
public class RedisListDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// LPUSH / RPUSH — tambah elemen ke kiri / kanan
jedis.rpush("antrian:email", "email-1", "email-2", "email-3");
jedis.lpush("antrian:email", "email-prioritas"); // masuk ke depan
// LRANGE — ambil elemen dalam range (0 = pertama, -1 = terakhir)
java.util.List<String> semua = jedis.lrange("antrian:email", 0, -1);
System.out.println("Isi antrian: " + semua);
// [email-prioritas, email-1, email-2, email-3]
// LLEN — panjang list
long panjang = jedis.llen("antrian:email");
System.out.println("Jumlah: " + panjang);
// LPOP / RPOP — ambil dan hapus dari kiri / kanan
String pertama = jedis.lpop("antrian:email"); // dequeue dari depan
System.out.println("Diproses: " + pertama); // email-prioritas
// BLPOP — blocking pop — tunggu sampai ada elemen (ideal untuk worker)
// Jika list kosong, blokir sampai ada elemen atau timeout (dalam detik)
java.util.List<String> hasil = jedis.blpop(5, "antrian:email", "antrian:sms");
if (hasil != null) {
System.out.println("Queue: " + hasil.get(0) + " | Pesan: " + hasil.get(1));
}
// LINSERT — sisipkan elemen sebelum / sesudah elemen tertentu
jedis.linsert("antrian:email", redis.clients.jedis.args.ListDirection.BEFORE,
"email-2", "email-1.5");
// LTRIM — potong list, simpan hanya elemen dalam range
// Berguna untuk menjaga list tidak tumbuh tak terbatas
jedis.rpush("log:aktivitas", "login", "buka-halaman", "logout");
jedis.ltrim("log:aktivitas", 0, 99); // simpan hanya 100 terakhir
}
}
}
Pattern: Simple Task Queue dengan List #
public class RedisTaskQueue {
private static final String QUEUE_KEY = "tasks:pending";
private static final String PROCESSING_KEY = "tasks:processing";
private final JedisPool pool;
public RedisTaskQueue(JedisPool pool) {
this.pool = pool;
}
// Producer: tambah task ke antrian
public void enqueue(String task) {
try (Jedis jedis = pool.getResource()) {
jedis.rpush(QUEUE_KEY, task);
}
}
// Consumer: ambil dan proses task (blocking)
public void startWorker() {
System.out.println("Worker siap memproses task...");
while (true) {
try (Jedis jedis = pool.getResource()) {
// BRPOPLPUSH — atomic: ambil dari queue, pindah ke processing list
// Jika worker crash, task masih ada di processing list
String task = jedis.brpoplpush(QUEUE_KEY, PROCESSING_KEY, 5);
if (task == null) continue; // timeout, coba lagi
try {
System.out.println("Memproses: " + task);
doWork(task);
// Hapus dari processing list setelah selesai
jedis.lrem(PROCESSING_KEY, 1, task);
} catch (Exception e) {
System.err.println("Gagal proses task: " + e.getMessage());
// Task tetap di processing list untuk recovery manual
}
}
}
}
private void doWork(String task) throws Exception {
Thread.sleep(100); // simulasi kerja
}
}
Struktur Data Set #
Set adalah kumpulan string unik tanpa urutan. Cocok untuk tag, membership check, dan operasi himpunan (union, intersection, difference).
public class RedisSetDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// SADD — tambah satu atau lebih member
jedis.sadd("tag:artikel:1", "java", "redis", "backend", "tutorial");
jedis.sadd("tag:artikel:2", "java", "spring", "backend", "microservice");
// SMEMBERS — ambil semua member
java.util.Set<String> tags = jedis.smembers("tag:artikel:1");
System.out.println("Tags: " + tags);
// SISMEMBER — cek keanggotaan — O(1)
boolean adaJava = jedis.sismember("tag:artikel:1", "java");
System.out.println("Punya tag java: " + adaJava);
// SCARD — jumlah member
long jumlah = jedis.scard("tag:artikel:1");
System.out.println("Jumlah tag: " + jumlah);
// Operasi himpunan
// SINTER — irisan (tags yang ada di KEDUA artikel)
java.util.Set<String> irisan = jedis.sinter("tag:artikel:1", "tag:artikel:2");
System.out.println("Tags sama: " + irisan); // [java, backend]
// SUNION — gabungan (semua tags dari semua artikel)
java.util.Set<String> gabungan = jedis.sunion("tag:artikel:1", "tag:artikel:2");
System.out.println("Semua tags: " + gabungan);
// SDIFF — selisih (tags di artikel 1 yang tidak ada di artikel 2)
java.util.Set<String> selisih = jedis.sdiff("tag:artikel:1", "tag:artikel:2");
System.out.println("Tags unik artikel 1: " + selisih); // [redis, tutorial]
// SREM — hapus member
jedis.srem("tag:artikel:1", "tutorial");
// SRANDMEMBER — ambil member acak (untuk rekomendasi, sampling)
String acak = jedis.srandmember("tag:artikel:1");
java.util.List<String> beberapaAcak = jedis.srandmember("tag:artikel:1", 2);
}
}
}
Struktur Data Sorted Set #
Sorted Set seperti Set tapi setiap member punya score (angka float). Member diurutkan berdasarkan score. Ini adalah struktur data paling serbaguna di Redis — cocok untuk leaderboard, priority queue, dan range query berdasarkan score.
public class RedisSortedSetDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// ZADD — tambah member dengan score
jedis.zadd("leaderboard:game1", 1500.0, "pemain:alice");
jedis.zadd("leaderboard:game1", 2300.0, "pemain:budi");
jedis.zadd("leaderboard:game1", 1800.0, "pemain:citra");
jedis.zadd("leaderboard:game1", 3100.0, "pemain:dani");
jedis.zadd("leaderboard:game1", 2100.0, "pemain:eka");
// ZRANGE — ambil member berdasarkan rank (ascending, index 0 = score terendah)
java.util.List<String> terendah = jedis.zrange("leaderboard:game1", 0, -1);
System.out.println("Urutan terendah ke tertinggi: " + terendah);
// ZREVRANGE — urutan tertinggi ke terendah (top players)
java.util.List<String> top3 = jedis.zrevrange("leaderboard:game1", 0, 2);
System.out.println("Top 3: " + top3); // [dani, budi, eka]
// ZRANGEBYSCORE — ambil member dengan score dalam range tertentu
java.util.List<String> tengah = jedis.zrangeByScore(
"leaderboard:game1", 1800, 2300);
System.out.println("Score 1800-2300: " + tengah);
// ZSCORE — ambil score satu member
Double score = jedis.zscore("leaderboard:game1", "pemain:alice");
System.out.println("Score alice: " + score);
// ZRANK / ZREVRANK — posisi dalam ranking (0-based)
Long rankAsc = jedis.zrank("leaderboard:game1", "pemain:budi");
Long rankDesc = jedis.zrevrank("leaderboard:game1", "pemain:budi");
System.out.println("Rank budi (asc): " + rankAsc + " | (desc): " + rankDesc);
// ZINCRBY — tambah score secara atomic (untuk update score real-time)
jedis.zincrBy("leaderboard:game1", 500.0, "pemain:alice");
// ZCARD — jumlah member
long total = jedis.zcard("leaderboard:game1");
// ZCOUNT — hitung member dalam range score
long count = jedis.zcount("leaderboard:game1", 2000, 3000);
System.out.println("Pemain dengan score 2000-3000: " + count);
// ZREM — hapus member
jedis.zrem("leaderboard:game1", "pemain:eka");
}
}
}
TTL dan Eviction Policy #
Redis di production harus dikonfigurasi dengan benar untuk menghindari kehabisan memori.
Mengatur TTL #
public class RedisTTLDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// Set TTL saat membuat kunci
jedis.setex("session:user123", 1800, "data-session"); // 30 menit
// Tambahkan TTL ke kunci yang sudah ada
jedis.set("cache:produk:456", "{...}");
jedis.expire("cache:produk:456", 300); // 5 menit
// Hapus TTL — jadikan kunci permanen kembali
jedis.persist("cache:produk:456");
// Cek TTL
long ttl = jedis.ttl("session:user123"); // dalam detik, -1 = tidak ada TTL, -2 = tidak ada kunci
long pttl = jedis.pttl("session:user123"); // dalam milidetik
// Cek kapan kunci expire (Unix timestamp milidetik)
jedis.expireAt("token:abc", System.currentTimeMillis() / 1000 + 3600); // expire 1 jam dari sekarang
}
}
}
Eviction Policy #
Ketika Redis mencapai batas memori (maxmemory), ia menjalankan eviction policy untuk menghapus kunci secara otomatis. Pilih policy yang sesuai:
noeviction → tolak write baru jika memori penuh (default, buruk untuk cache)
allkeys-lru → hapus kunci yang paling lama tidak diakses (LRU) dari semua kunci
volatile-lru → hapus kunci ber-TTL yang paling lama tidak diakses
allkeys-lfu → hapus kunci yang paling jarang diakses (LFU) dari semua kunci
volatile-lfu → hapus kunci ber-TTL yang paling jarang diakses
allkeys-random → hapus kunci secara acak dari semua kunci
volatile-random → hapus kunci ber-TTL secara acak
volatile-ttl → hapus kunci yang paling dekat expire-nya
Untuk kasus cache umum, allkeys-lru atau allkeys-lfu adalah pilihan terbaik:
# Di redis.conf
maxmemory 512mb
maxmemory-policy allkeys-lru
# Atau via CLI
redis-cli CONFIG SET maxmemory 512mb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
Pola Caching #
Cache-Aside (Lazy Loading) #
Pattern paling umum — data dimuat ke cache hanya ketika dibutuhkan:
public class CacheAsidePattern {
private final JedisPool pool;
private final DataRepository repository; // simulasi akses ke database
public CacheAsidePattern(JedisPool pool, DataRepository repository) {
this.pool = pool;
this.repository = repository;
}
public String getProduk(String produkId) {
String cacheKey = "cache:produk:" + produkId;
try (Jedis jedis = pool.getResource()) {
// 1. Cek cache terlebih dahulu
String cached = jedis.get(cacheKey);
if (cached != null) {
System.out.println("Cache HIT untuk produk " + produkId);
return cached;
}
// 2. Cache MISS — ambil dari database
System.out.println("Cache MISS untuk produk " + produkId);
String data = repository.findProdukById(produkId);
if (data != null) {
// 3. Simpan ke cache dengan TTL
jedis.setex(cacheKey, 300, data); // cache 5 menit
}
return data;
}
}
// Invalidasi cache ketika data berubah
public void updateProduk(String produkId, String data) {
repository.updateProduk(produkId, data);
try (Jedis jedis = pool.getResource()) {
// Hapus cache lama — akan di-reload saat diakses berikutnya
jedis.del("cache:produk:" + produkId);
}
}
interface DataRepository {
String findProdukById(String id);
void updateProduk(String id, String data);
}
}
Write-Through #
Data ditulis ke cache dan database secara bersamaan:
public class WriteThroughPattern {
private final JedisPool pool;
private final DataRepository repository;
private static final int CACHE_TTL = 600; // 10 menit
public WriteThroughPattern(JedisPool pool, DataRepository repository) {
this.pool = pool;
this.repository = repository;
}
public void simpanProduk(String produkId, String data) {
// Tulis ke database
repository.updateProduk(produkId, data);
// Langsung perbarui cache — tidak ada jeda data stale
try (Jedis jedis = pool.getResource()) {
jedis.setex("cache:produk:" + produkId, CACHE_TTL, data);
}
}
public String getProduk(String produkId) {
try (Jedis jedis = pool.getResource()) {
String cached = jedis.get("cache:produk:" + produkId);
if (cached != null) return cached;
return repository.findProdukById(produkId);
}
}
interface DataRepository {
String findProdukById(String id);
void updateProduk(String id, String data);
}
}
Distributed Lock #
Redis sering digunakan untuk implementasi distributed lock — memastikan hanya satu proses yang mengeksekusi bagian kritis pada satu waktu di lingkungan terdistribusi.
import java.util.UUID;
public class RedisDistributedLock {
private final JedisPool pool;
private static final String LOCK_PREFIX = "lock:";
private static final int DEFAULT_TIMEOUT_MS = 5000; // 5 detik
public RedisDistributedLock(JedisPool pool) {
this.pool = pool;
}
// Coba dapatkan lock
// Mengembalikan lockToken jika berhasil, null jika lock sudah dipegang proses lain
public String tryAcquire(String resourceName, int ttlSeconds) {
String lockKey = LOCK_PREFIX + resourceName;
String lockToken = UUID.randomUUID().toString(); // unique token untuk identifikasi pemilik
try (Jedis jedis = pool.getResource()) {
// SET NX EX — set hanya jika belum ada, dengan TTL
// Ini atomic — tidak ada race condition antara cek dan set
String result = jedis.set(
lockKey,
lockToken,
redis.clients.jedis.params.SetParams.setParams()
.nx() // hanya set jika kunci BELUM ada
.ex(ttlSeconds) // TTL otomatis — lock tidak menggantung selamanya
);
if ("OK".equals(result)) {
System.out.println("Lock diperoleh: " + resourceName + " (token: " + lockToken + ")");
return lockToken;
}
System.out.println("Lock tidak tersedia: " + resourceName);
return null;
}
}
// Lepaskan lock — hanya jika token cocok (kamu yang pegang locknya)
public boolean release(String resourceName, String lockToken) {
String lockKey = LOCK_PREFIX + resourceName;
try (Jedis jedis = pool.getResource()) {
// ANTI-PATTERN: GET lalu DEL — ada race condition di antara keduanya
// String current = jedis.get(lockKey);
// if (lockToken.equals(current)) jedis.del(lockKey); // ← tidak atomic!
// ✓ BENAR: gunakan Lua script untuk operasi GET + DEL yang atomic
String luaScript =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(luaScript,
java.util.List.of(lockKey),
java.util.List.of(lockToken)
);
boolean released = Long.valueOf(1).equals(result);
if (released) {
System.out.println("Lock dilepas: " + resourceName);
} else {
System.out.println("Gagal lepas lock — bukan pemilik atau sudah expired: " + resourceName);
}
return released;
}
}
// Eksekusi blok kode dengan lock
public <T> T withLock(String resourceName, int ttlSeconds,
java.util.concurrent.Callable<T> action) throws Exception {
String token = null;
long deadline = System.currentTimeMillis() + DEFAULT_TIMEOUT_MS;
// Retry sampai lock tersedia atau timeout
while (System.currentTimeMillis() < deadline) {
token = tryAcquire(resourceName, ttlSeconds);
if (token != null) break;
Thread.sleep(50 + (long)(Math.random() * 50)); // jitter untuk hindari thundering herd
}
if (token == null) {
throw new RuntimeException("Gagal mendapatkan lock untuk: " + resourceName);
}
try {
return action.call();
} finally {
release(resourceName, token);
}
}
}
sequenceDiagram
participant A as Service A
participant B as Service B
participant R as Redis
A->>R: SET lock:order NX EX 30 "token-A"
R-->>A: OK (lock diperoleh)
B->>R: SET lock:order NX EX 30 "token-B"
R-->>B: nil (lock sudah ada)
Note over A: eksekusi logika kritis
A->>R: EVAL lua — DEL jika token cocok
R-->>A: 1 (berhasil dilepas)
B->>R: SET lock:order NX EX 30 "token-B"
R-->>B: OK (sekarang bisa dapatkan lock)Pipeline — Mengurangi Round-Trip #
Setiap perintah Redis membutuhkan satu round-trip ke server. Pipeline mengirimkan banyak perintah sekaligus dan membaca semua response di akhir — sangat efektif untuk operasi batch.
public class RedisPipelineDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// ✗ ANTI-PATTERN: kirim satu per satu — N round-trip untuk N perintah
for (int i = 0; i < 100; i++) {
jedis.set("key:" + i, "value:" + i);
}
// ✓ BENAR: pipeline — 1 round-trip untuk 100 perintah
redis.clients.jedis.Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 100; i++) {
pipeline.set("key:" + i, "value:" + i);
pipeline.expire("key:" + i, 300);
}
// Kirim semua sekaligus dan tunggu response
java.util.List<Object> responses = pipeline.syncAndReturnAll();
System.out.println("Pipeline selesai: " + responses.size() + " respons diterima");
}
}
// Pipeline untuk batch read
public java.util.List<String> batchGet(JedisPool pool, java.util.List<String> keys) {
try (Jedis jedis = pool.getResource()) {
redis.clients.jedis.Pipeline pipeline = jedis.pipelined();
java.util.List<redis.clients.jedis.Response<String>> futures = new java.util.ArrayList<>();
for (String key : keys) {
futures.add(pipeline.get(key));
}
pipeline.sync();
java.util.List<String> results = new java.util.ArrayList<>();
for (redis.clients.jedis.Response<String> future : futures) {
results.add(future.get());
}
return results;
}
}
}
Pub/Sub di Redis #
Redis mendukung pola publish/subscribe sederhana. Berbeda dari Kafka atau RabbitMQ, Redis Pub/Sub tidak menyimpan pesan — jika tidak ada subscriber saat pesan dikirim, pesan hilang. Cocok untuk notifikasi real-time, invalidasi cache, dan event sederhana.
import redis.clients.jedis.JedisPubSub;
public class RedisPubSubDemo {
// Subscriber — berjalan di thread terpisah
public static class NotificationSubscriber extends JedisPubSub {
@Override
public void onMessage(String channel, String message) {
System.out.printf("[%s] Pesan diterima: %s%n", channel, message);
// Proses pesan
}
@Override
public void onSubscribe(String channel, int subscribedChannels) {
System.out.println("Berlangganan ke channel: " + channel);
}
@Override
public void onUnsubscribe(String channel, int subscribedChannels) {
System.out.println("Berhenti dari channel: " + channel);
}
}
public static void startSubscriber(JedisPool pool) {
// Subscribe harus berjalan di thread terpisah karena operasinya blocking
Thread subscriberThread = new Thread(() -> {
try (Jedis jedis = pool.getResource()) {
NotificationSubscriber subscriber = new NotificationSubscriber();
// Subscribe ke satu atau lebih channel
jedis.subscribe(subscriber, "notif:order", "notif:payment");
// atau subscribe menggunakan pattern
// jedis.psubscribe(subscriber, "notif:*");
}
});
subscriberThread.setDaemon(true);
subscriberThread.start();
}
public static void publish(JedisPool pool, String channel, String message) {
try (Jedis jedis = pool.getResource()) {
long subscriberCount = jedis.publish(channel, message);
System.out.printf("Pesan terkirim ke %d subscriber di channel '%s'%n",
subscriberCount, channel);
}
}
}
Kapan Menggunakan Redis dan Kapan Tidak #
GUNAKAN REDIS JIKA:
✓ Cache layer untuk mengurangi beban database (session, hasil query, halaman)
✓ Rate limiting — counter atomic per IP/user dengan TTL
✓ Distributed lock — koordinasi antar instance/pod
✓ Leaderboard dan ranking — Sorted Set dengan ZINCRBY
✓ Session store — Hash per session dengan TTL
✓ Simple task queue — List dengan BRPOPLPUSH
✓ Pub/Sub ringan — notifikasi real-time, cache invalidation
✓ Counting dan analytics sederhana — INCR, HINCRBY
PERTIMBANGKAN ALTERNATIF JIKA:
✗ Data tidak boleh hilang sama sekali → database relasional
✗ Memori sangat terbatas dan data besar → Memcached (lebih hemat memori)
✗ Butuh query kompleks atas data yang dicache → simpan di DB, jangan di Redis
✗ Butuh message queue yang andal dengan DLQ → RabbitMQ, Kafka, atau SQS
✗ Data lebih besar dari RAM yang tersedia → Redis tidak cocok sebagai primary store
Ringkasan #
- Gunakan JedisPool, bukan membuat koneksi baru setiap request — buka Jedis dalam try-with-resources agar otomatis dikembalikan ke pool setelah selesai.
- String cocok untuk counter atomic (
INCR), session token, dan cache sederhana. Hash cocok untuk objek dengan banyak field yang diperbarui secara individual.- List mendukung queue dengan
RPUSH/BLPOPdan stack denganLPUSH/LPOP.BLPOPadalah cara efisien untuk worker queue tanpa busy-loop.- Set untuk membership check O(1) dan operasi himpunan (union, intersection, difference). Sorted Set untuk leaderboard, priority queue, dan range query berdasarkan score.
- Selalu set TTL pada kunci cache — gunakan
SETEXatauSET ... EX. Konfigurasimaxmemory-policy allkeys-lruuntuk cache yang mengelola memori secara otomatis.- Cache-aside adalah pattern paling umum — baca dari cache, jika miss baru ke database, lalu simpan ke cache. Selalu invalidasi cache saat data berubah.
- Distributed lock menggunakan
SET NX EX(atomic) dan Lua script untuk release yang aman — jangan gunakan GET + DEL terpisah karena ada race condition.- Pipeline mengurangi round-trip secara drastis untuk operasi batch — gunakan saat perlu set atau get banyak kunci sekaligus.
- Redis Pub/Sub cocok untuk notifikasi real-time ringan, bukan sebagai pengganti message broker yang andal — pesan hilang jika tidak ada subscriber.