Redis

Redis #

Redis (Remote Dictionary Server) adalah penyimpanan data in-memory yang sangat cepat — latensi di bawah 1 milidetik untuk sebagian besar operasi. Ia mendukung berbagai struktur data: String, Hash, List, Set, Sorted Set, dan lebih. Redis digunakan untuk caching hasil query database, menyimpan sesi pengguna, rate limiting, distributed lock, leaderboard real-time, dan bahkan sebagai message broker sederhana. Di Kotlin, ada dua library populer: Lettuce (async, thread-safe, berbasis Netty, direkomendasikan) dan Jedis (synchronous, lebih sederhana). Artikel ini membahas keduanya dengan fokus pada Lettuce, mencakup semua tipe data Redis, pola caching yang umum, dan penggunaan lanjutan.

Kapan Menggunakan Redis #

GUNAKAN Redis untuk:
  ✓ Cache hasil query database yang mahal (reduce DB load)
  ✓ Session store (lebih skalabel dari session in-memory)
  ✓ Rate limiting (mencegah abuse API)
  ✓ Distributed lock (koordinasi antar instance aplikasi)
  ✓ Leaderboard/ranking (Sorted Set)
  ✓ Queue sederhana (List dengan LPUSH/BRPOP)
  ✓ Real-time counter (INCR, DECR)
  ✓ Pub/Sub sederhana antar service

JANGAN pakai Redis untuk:
  ✗ Data primer (Redis in-memory, data hilang jika server mati tanpa persistence)
  ✗ Query kompleks dengan join antar entitas
  ✗ Data yang tidak boleh hilang tanpa backup strategy
  ✗ Data yang melebihi kapasitas RAM server

Setup dan Dependensi #

// build.gradle.kts
dependencies {
    // Lettuce — direkomendasikan (async, thread-safe)
    implementation("io.lettuce:lettuce-core:6.3.2.RELEASE")

    // Jedis — alternatif yang lebih sederhana (sync)
    // implementation("redis.clients:jedis:5.1.2")

    // kotlinx.serialization untuk serialisasi nilai
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

    // Coroutine
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}

Koneksi dengan Lettuce #

import io.lettuce.core.RedisClient
import io.lettuce.core.RedisURI
import io.lettuce.core.api.StatefulRedisConnection
import io.lettuce.core.api.sync.RedisCommands
import io.lettuce.core.support.ConnectionPoolSupport
import org.apache.commons.pool2.impl.GenericObjectPool
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
import java.time.Duration

object KoneksiRedis {
    private val uri = RedisURI.builder()
        .withHost(System.getenv("REDIS_HOST") ?: "localhost")
        .withPort(System.getenv("REDIS_PORT")?.toInt() ?: 6379)
        .also { builder ->
            System.getenv("REDIS_PASSWORD")?.let { builder.withPassword(it.toCharArray()) }
        }
        .withDatabase(System.getenv("REDIS_DB")?.toInt() ?: 0)
        .withTimeout(Duration.ofSeconds(10))
        .build()

    private val client = RedisClient.create(uri)

    // Satu koneksi bisa digunakan dari satu thread — gunakan pool untuk multi-thread
    val koneksi: StatefulRedisConnection<String, String> by lazy {
        client.connect()
    }

    // Pool koneksi untuk lingkungan multi-thread
    val pool: GenericObjectPool<StatefulRedisConnection<String, String>> by lazy {
        val config = GenericObjectPoolConfig<StatefulRedisConnection<String, String>>().apply {
            maxTotal = 10      // maks 10 koneksi
            maxIdle = 5        // maks 5 koneksi idle
            minIdle = 2        // min 2 koneksi selalu siap
            testOnBorrow = true
        }
        ConnectionPoolSupport.createGenericObjectPool({ client.connect() }, config)
    }

    // Gunakan koneksi dari pool secara aman
    fun <T> gunakan(blok: (RedisCommands<String, String>) -> T): T {
        return pool.borrowObject().use { conn ->
            blok(conn.sync())
        }
    }

    fun tutup() {
        pool.close()
        client.shutdown()
    }
}

fun main() {
    val info = KoneksiRedis.gunakan { redis ->
        redis.ping()
    }
    println("Redis OK: $info")  // PONG
}

Tipe Data String — Paling Dasar #

String adalah tipe data Redis yang paling umum. Bisa menyimpan teks, angka, atau data biner:

fun contohString() {
    KoneksiRedis.gunakan { redis ->
        // SET dan GET
        redis.set("nama", "Budi Santoso")
        println(redis.get("nama"))  // Budi Santoso

        // SET dengan TTL (expire dalam detik)
        redis.setex("sesi:usr123", 3600, "data sesi pengguna")  // expire 1 jam
        println(redis.ttl("sesi:usr123"))  // ~3600 detik tersisa

        // SET hanya jika key belum ada (atomic)
        val berhasil = redis.setnx("kunci.unik", "nilai")
        println(berhasil)  // true jika berhasil, false jika key sudah ada

        // SETEX dengan kondisi NX atau XX
        redis.set("kunci", "nilai", io.lettuce.core.SetArgs.Builder.nx().ex(60))
        // NX = hanya jika tidak ada, EX = expire 60 detik

        // INCR dan DECR — atomik, aman untuk counter
        redis.set("pengunjung", "0")
        redis.incr("pengunjung")          // 1
        redis.incrby("pengunjung", 5)     // 6
        redis.decr("pengunjung")          // 5
        println(redis.get("pengunjung"))  // 5

        // DEL — hapus key
        redis.del("nama", "pengunjung")

        // EXISTS — cek apakah key ada
        println(redis.exists("nama"))  // 0 (tidak ada)

        // KEYS dengan pattern (hati-hati di production!)
        redis.set("produk:1", "Laptop")
        redis.set("produk:2", "Mouse")
        redis.set("produk:3", "Keyboard")
        println(redis.keys("produk:*"))  // [produk:1, produk:2, produk:3]
        // JANGAN gunakan KEYS di production dengan data banyak — gunakan SCAN
    }
}

Tipe Data Hash — Objek Multi-Field #

Hash cocok untuk menyimpan objek dengan banyak field, seperti profil pengguna:

fun contohHash() {
    KoneksiRedis.gunakan { redis ->
        val key = "pengguna:1001"

        // HSET — set satu atau banyak field
        redis.hset(key, mapOf(
            "nama"    to "Budi Santoso",
            "email"   to "[email protected]",
            "umur"    to "28",
            "kota"    to "Jakarta"
        ))

        // HGET — ambil satu field
        println(redis.hget(key, "nama"))     // Budi Santoso

        // HMGET — ambil banyak field sekaligus
        val hasil = redis.hmget(key, "nama", "email", "kota")
        println(hasil)  // [Budi Santoso, [email protected], Jakarta]

        // HGETALL — ambil semua field sebagai Map
        val semuaField = redis.hgetall(key)
        semuaField.forEach { (field, nilai) -> println("$field: $nilai") }

        // HDEL — hapus field tertentu
        redis.hdel(key, "umur")

        // HEXISTS — cek apakah field ada
        println(redis.hexists(key, "email"))   // true
        println(redis.hexists(key, "umur"))    // false

        // HLEN — jumlah field
        println(redis.hlen(key))  // 3

        // HINCRBY — increment nilai numerik dalam hash
        redis.hset(key, "loginCount", "0")
        redis.hincrby(key, "loginCount", 1)
        println(redis.hget(key, "loginCount"))  // 1

        // Set TTL pada hash
        redis.expire(key, 3600)
    }
}

Tipe Data List — Antrian dan Tumpukan #

List adalah linked list yang mendukung operasi di kedua ujung:

fun contohList() {
    KoneksiRedis.gunakan { redis ->
        val key = "riwayat:pengguna:1001"

        // RPUSH — tambahkan ke kanan (tail)
        redis.rpush(key, "login-2024-01-01", "beli-produk", "logout")

        // LPUSH — tambahkan ke kiri (head) — untuk stack
        redis.lpush("notifikasi:inbox", "Pesanan terkonfirmasi", "Pembayaran berhasil")

        // LRANGE — ambil elemen dalam rentang (0 = pertama, -1 = terakhir)
        val semua = redis.lrange(key, 0, -1)
        println(semua)  // [login-2024-01-01, beli-produk, logout]

        // LLEN — panjang list
        println(redis.llen(key))  // 3

        // LPOP / RPOP — ambil dan hapus dari kiri/kanan
        val pertama = redis.lpop(key)  // login-2024-01-01
        val terakhir = redis.rpop(key) // logout

        // LINDEX — akses elemen di indeks tertentu
        println(redis.lindex(key, 0))  // beli-produk (sisa setelah 2 pop)

        // BRPOP — blocking pop: tunggu sampai ada elemen (untuk task queue)
        // redis.brpop(5, "antrian:tugas")  // tunggu maks 5 detik

        // LTRIM — pertahankan hanya elemen dalam rentang (trim list)
        redis.rpush("log:recent", "event1", "event2", "event3", "event4", "event5")
        redis.ltrim("log:recent", 0, 2)  // pertahankan hanya 3 elemen pertama
        println(redis.lrange("log:recent", 0, -1))  // [event1, event2, event3]
    }
}

Tipe Data Set — Koleksi Unik #

Set menyimpan elemen unik tanpa urutan:

fun contohSet() {
    KoneksiRedis.gunakan { redis ->
        // SADD — tambahkan elemen
        redis.sadd("tag:produk:1", "elektronik", "laptop", "gaming")
        redis.sadd("tag:produk:2", "elektronik", "mouse", "wireless")

        // SMEMBERS — ambil semua elemen
        println(redis.smembers("tag:produk:1"))  // [elektronik, laptop, gaming]

        // SISMEMBER — cek keanggotaan
        println(redis.sismember("tag:produk:1", "laptop"))     // true
        println(redis.sismember("tag:produk:1", "keyboard"))   // false

        // SCARD — jumlah elemen
        println(redis.scard("tag:produk:1"))  // 3

        // Operasi himpunan
        val irisan = redis.sinter("tag:produk:1", "tag:produk:2")  // {elektronik}
        val gabungan = redis.sunion("tag:produk:1", "tag:produk:2") // semua tag
        val selisih = redis.sdiff("tag:produk:1", "tag:produk:2")   // {laptop, gaming}

        println("Irisan: $irisan")
        println("Gabungan: $gabungan")
        println("Selisih: $selisih")

        // SREM — hapus elemen
        redis.srem("tag:produk:1", "gaming")

        // SPOP — ambil dan hapus elemen acak
        val acak = redis.spop("tag:produk:1")
        println("Elemen acak: $acak")
    }
}

Tipe Data Sorted Set — Ranking dan Leaderboard #

Sorted Set adalah Set dengan skor numerik untuk setiap elemen, selalu terurut:

fun contohSortedSet() {
    KoneksiRedis.gunakan { redis ->
        val key = "leaderboard:game"

        // ZADD — tambahkan dengan skor
        redis.zadd(key, 1500.0, "Budi")
        redis.zadd(key, 2300.0, "Sari")
        redis.zadd(key, 1800.0, "Ahmad")
        redis.zadd(key, 2800.0, "Rina")
        redis.zadd(key, 1200.0, "Doni")

        // ZRANGE — ambil dalam urutan skor (ascending)
        println(redis.zrange(key, 0, -1))
        // [Doni, Budi, Ahmad, Sari, Rina]

        // ZREVRANGE — urutan descending (skor tertinggi duluan)
        val top3 = redis.zrevrange(key, 0, 2)
        println("Top 3: $top3")  // [Rina, Sari, Ahmad]

        // ZRANGEBYSCORE — filter berdasarkan rentang skor
        val menengah = redis.zrangebyscore(key,
            io.lettuce.core.Range.create(1500.0, 2500.0)
        )
        println("Skor 1500-2500: $menengah")  // [Budi, Ahmad, Sari]

        // ZSCORE — ambil skor elemen tertentu
        println(redis.zscore(key, "Rina"))  // 2800.0

        // ZRANK / ZREVRANK — posisi dalam urutan (0-based)
        println(redis.zrevrank(key, "Rina"))  // 0 (tertinggi)
        println(redis.zrevrank(key, "Budi"))  // 3

        // ZINCRBY — tambah skor secara atomik
        redis.zincrby(key, 500.0, "Budi")
        println(redis.zscore(key, "Budi"))  // 2000.0

        // ZCARD — jumlah elemen
        println(redis.zcard(key))  // 5

        // Contoh leaderboard dengan skor dan rank
        println("\n=== LEADERBOARD ===")
        redis.zrevrangeWithScores(key, 0, 4).forEachIndexed { i, scored ->
            println("${i + 1}. ${scored.value}: ${scored.score.toInt()} poin")
        }
    }
}

Pola Caching Umum #

Cache-Aside (Lazy Loading) #

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Serializable
data class Produk(val id: Int, val nama: String, val harga: Double, val stok: Int)

class LayananProdukDenganCache(private val repo: ProdukRepository) {
    private val json = Json { ignoreUnknownKeys = true }
    private val TTL_DETIK = 300L  // cache 5 menit

    fun ambilProduk(id: Int): Produk? {
        val kunciCache = "produk:$id"

        // 1. Cek cache dulu
        val dalamCache = KoneksiRedis.gunakan { redis -> redis.get(kunciCache) }
        if (dalamCache != null) {
            println("Cache HIT untuk produk $id")
            return json.decodeFromString(Produk.serializer(), dalamCache)
        }

        // 2. Cache MISS — ambil dari database
        println("Cache MISS untuk produk $id — ambil dari DB")
        val produk = repo.cariById(id) ?: return null

        // 3. Simpan ke cache
        val dataJson = json.encodeToString(Produk.serializer(), produk)
        KoneksiRedis.gunakan { redis ->
            redis.setex(kunciCache, TTL_DETIK, dataJson)
        }

        return produk
    }

    // Invalidasi cache saat data berubah
    fun perbaruiProduk(produk: Produk) {
        repo.perbarui(produk)

        // Hapus cache — akan di-populate ulang di request berikutnya
        KoneksiRedis.gunakan { redis ->
            redis.del("produk:${produk.id}")
        }
        println("Cache invalidasi untuk produk ${produk.id}")
    }

    // Cache banyak produk sekaligus dengan pipeline
    fun cachekanSemuaProduk(produk: List<Produk>) {
        KoneksiRedis.gunakan { redis ->
            // Pipeline: kirim banyak perintah sekaligus tanpa round-trip per perintah
            val pipeline = (redis as io.lettuce.core.api.sync.RedisCommands<String, String>)
            produk.forEach { p ->
                val dataJson = json.encodeToString(Produk.serializer(), p)
                redis.setex("produk:${p.id}", TTL_DETIK, dataJson)
            }
        }
        println("${produk.size} produk di-cache")
    }
}

Rate Limiting #

fun cekRateLimit(penggunaId: String, maks: Int = 100, windowDetik: Long = 60): Boolean {
    val key = "rate:$penggunaId:${System.currentTimeMillis() / (windowDetik * 1000)}"

    return KoneksiRedis.gunakan { redis ->
        val hitungan = redis.incr(key)

        if (hitungan == 1L) {
            // Set TTL hanya saat pertama kali (atomic-ish)
            redis.expire(key, windowDetik + 1)
        }

        if (hitungan > maks) {
            val sisaDetik = redis.ttl(key)
            println("Rate limit terlampaui untuk $penggunaId. Reset dalam $sisaDetik detik")
            false
        } else {
            println("$penggunaId: $hitungan/$maks request dalam window ini")
            true
        }
    }
}

Distributed Lock #

import java.util.UUID

class DistributedLock(private val lockName: String, private val ttlDetik: Long = 30) {
    private val lockKey = "lock:$lockName"
    private val lockValue = UUID.randomUUID().toString()  // nilai unik per instance

    fun acquire(): Boolean {
        return KoneksiRedis.gunakan { redis ->
            val hasil = redis.set(
                lockKey,
                lockValue,
                io.lettuce.core.SetArgs.Builder.nx().ex(ttlDetik)
            )
            hasil == "OK"
        }
    }

    fun release() {
        // Hanya hapus jika kita yang punya lock (cek lockValue)
        // Idealnya gunakan Lua script untuk atomisitas
        KoneksiRedis.gunakan { redis ->
            val nilai = redis.get(lockKey)
            if (nilai == lockValue) {
                redis.del(lockKey)
                println("Lock '$lockName' dilepas")
            } else {
                println("Lock '$lockName' sudah expired atau dimiliki pihak lain")
            }
        }
    }
}

fun <T> denganLock(namaLock: String, blok: () -> T): T? {
    val lock = DistributedLock(namaLock)
    return if (lock.acquire()) {
        try {
            blok()
        } finally {
            lock.release()
        }
    } else {
        println("Gagal mendapatkan lock '$namaLock' — proses lain sedang berjalan")
        null
    }
}

// Penggunaan
fun main() {
    denganLock("proses-laporan-bulanan") {
        println("Memproses laporan bulanan...")
        Thread.sleep(2000)
        println("Laporan selesai")
    }
}

Pub/Sub Redis #

Redis juga mendukung publish-subscribe sederhana:

import io.lettuce.core.pubsub.StatefulRedisPubSubConnection
import io.lettuce.core.pubsub.RedisPubSubListener

fun contohPubSub() {
    val client = io.lettuce.core.RedisClient.create("redis://localhost:6379")

    // Publisher menggunakan koneksi biasa
    val publisherConn = client.connect()
    val publisher = publisherConn.sync()

    // Subscriber menggunakan koneksi Pub/Sub khusus
    val subscriberConn: StatefulRedisPubSubConnection<String, String> = client.connectPubSub()

    subscriberConn.addListener(object : RedisPubSubListener<String, String> {
        override fun message(channel: String, message: String) {
            println("[$channel] $message")
        }
        override fun message(pattern: String, channel: String, message: String) {}
        override fun subscribed(channel: String, count: Long) {
            println("Berlangganan ke '$channel' (total: $count)")
        }
        override fun psubscribed(pattern: String, count: Long) {}
        override fun unsubscribed(channel: String, count: Long) {}
        override fun punsubscribed(pattern: String, count: Long) {}
    })

    // Subscribe ke channel
    val subscriberAsync = subscriberConn.async()
    subscriberAsync.subscribe("notifikasi", "sistem.alert").get()

    // Publish pesan
    Thread.sleep(100)
    publisher.publish("notifikasi", "Pesanan baru masuk!")
    publisher.publish("sistem.alert", "CPU usage tinggi!")

    Thread.sleep(500)
    subscriberConn.close()
    publisherConn.close()
    client.shutdown()
}

Ringkasan #

  • Lettuce untuk production, Jedis untuk prototipe — Lettuce thread-safe dan async, satu instance bisa dibagikan ke seluruh aplikasi. Jedis perlu connection pool untuk multi-thread.
  • Selalu set TTL — hampir semua key Redis harus punya waktu kedaluwarsa. Tanpa TTL, Redis bisa kehabisan memori. Gunakan setex, expire, atau pexpire.
  • Hash untuk objek — daripada menyimpan satu objek JSON per key (String), gunakan Hash jika perlu memperbarui field individual tanpa menulis ulang seluruh objek.
  • Sorted Set untuk ranking — leaderboard, top-N, atau antrian berprioritas adalah use case sempurna untuk Sorted Set. ZINCRBY untuk increment skor secara atomik.
  • SCAN bukan KEYS di productionKEYS pattern memblokir Redis selama eksekusi. Untuk data besar, gunakan SCAN yang iteratif dan non-blocking.
  • Pipeline untuk operasi massal — daripada mengirim 1000 perintah secara terpisah (1000 round-trip), gunakan pipeline untuk mengirim semuanya sekaligus dan baca respons sekaligus.
  • Distributed lock dengan NX dan EXSET kunci nilai NX EX 30 adalah operasi atomik yang sempurna untuk distributed lock. NX = hanya jika tidak ada, EX = expire otomatis mencegah deadlock.
  • TTL pendek untuk cache, TTL panjang untuk state — cache hasil query: 5-15 menit. Session pengguna: sesuai session timeout aplikasi. Distributed lock: sedikit lebih lama dari estimasi waktu proses.

← Sebelumnya: Google Pub/Sub   Berikutnya: Memcached →

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