Redis

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.

JedisLettuce
Model I/OSynchronous (blocking)Asynchronous & Reactive
Thread safetyTidak — butuh connection poolYa — satu connection bisa dipakai banyak thread
Spring Boot defaultTidak (dulu ya, sekarang Lettuce)Ya (sejak Spring Boot 2)
KemudahanSangat mudah, API intuitifSedikit lebih kompleks
Throughput tinggiButuh pool besarEfisien dengan sedikit connection
Redis ClusterDidukungDidukung, 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/BLPOP dan stack dengan LPUSH/LPOP. BLPOP adalah 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 SETEX atau SET ... EX. Konfigurasi maxmemory-policy allkeys-lru untuk 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.

← Sebelumnya: Google Pub/Sub   Berikutnya: Memcached →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact