Memcached

Memcached #

Memcached adalah sistem caching in-memory terdistribusi yang sangat sederhana dan sangat cepat. Ia lahir sebelum Redis dan dirancang dengan satu filosofi: lakukan satu hal dengan sangat baik — menyimpan pasangan key-value di memori dengan latensi minimal. Memcached tidak punya tipe data kompleks seperti Redis, tidak ada persistence, tidak ada pub/sub, tidak ada Lua scripting. Tapi justru kesederhanaannya itulah yang membuatnya sangat ringan dan mudah di-scale secara horizontal — tambah server Memcached baru, dan client secara otomatis mendistribusikan data menggunakan consistent hashing. Di Kotlin, dua library paling umum adalah Spymemcached (library lama yang masih banyak digunakan) dan XMemcached (lebih modern, mendukung operasi asinkron dan NIO).

Memcached vs Redis — Kapan Memilih #

flowchart TD
    A{Kebutuhan Caching?} --> B{Butuh tipe data\nkompleks?}
    B -- Ya --> C[Redis\nHash, List, Set, Sorted Set]
    B -- Tidak --> D{Butuh persistence\natau replication?}
    D -- Ya --> C
    D -- Tidak --> E{Butuh horizontal\nscaling yang mudah?}
    E -- Ya --> F[Memcached\nSimple, scalable, ringan]
    E -- Tidak --> G{Tim familiar\ndengan Redis?}
    G -- Ya --> C
    G -- Tidak --> F
AspekMemcachedRedis
Tipe dataKey-value (String saja)String, Hash, List, Set, ZSet, dll
Persistence✗ Tidak ada✓ RDB dan AOF
Replication✗ Tidak native✓ Master-Replica
Clustering✓ Horizontal scaling mudah✓ Redis Cluster
ThreadingMulti-threadSingle-thread (Redis 6+ multi-IO)
Overhead memoriSangat rendahSedikit lebih tinggi
Pub/Sub✗ Tidak ada✓ Ada
Atomic operationsCAS, appendINCR, DECR, GETSET, banyak lagi
Cocok untukPure caching, simple key-valueCaching + fitur tambahan
PILIH Memcached jika:
  ✓ Hanya butuh caching murni, tidak butuh fitur lain
  ✓ Tim familiar dengan Memcached dan setup sudah ada
  ✓ Butuh multi-threading native (Memcached lebih efisien CPU)
  ✓ Skala horizontal yang sangat mudah — tambah node = lebih kapasitas
  ✓ Data cache sederhana (string, serialized object)

PILIH Redis jika:
  ✓ Butuh tipe data kaya (Hash, List, Sorted Set)
  ✓ Butuh persistence (data bertahan setelah restart)
  ✓ Butuh replication dan high availability
  ✓ Butuh fitur tambahan: pub/sub, Lua, distributed lock

Setup dan Dependensi #

// build.gradle.kts
dependencies {
    // Spymemcached — library yang matur dan banyak digunakan
    implementation("net.spy:spymemcached:2.12.3")

    // XMemcached — alternatif modern dengan NIO
    // implementation("com.googlecode.xmemcached:xmemcached:2.4.7")

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

Koneksi dengan Spymemcached #

import net.spy.memcached.MemcachedClient
import net.spy.memcached.AddrUtil
import net.spy.memcached.ConnectionFactoryBuilder
import net.spy.memcached.DefaultHashAlgorithm
import net.spy.memcached.FailureMode
import java.net.InetSocketAddress

object KoneksiMemcached {

    // Koneksi ke satu server
    val client: MemcachedClient by lazy {
        val host = System.getenv("MEMCACHED_HOST") ?: "localhost"
        val port = System.getenv("MEMCACHED_PORT")?.toInt() ?: 11211
        MemcachedClient(InetSocketAddress(host, port))
    }

    // Koneksi ke banyak server (distributed)
    fun buatClientDistributed(servers: List<String>): MemcachedClient {
        // Format server: "host1:11211 host2:11211 host3:11211"
        val alamatServer = servers.joinToString(" ")
        return MemcachedClient(AddrUtil.getAddresses(alamatServer))
    }

    // Koneksi dengan konfigurasi lengkap
    fun buatClientKustom(): MemcachedClient {
        val factory = ConnectionFactoryBuilder()
            .setProtocol(ConnectionFactoryBuilder.Protocol.BINARY)  // protokol binary lebih efisien
            .setHashAlg(DefaultHashAlgorithm.KETAMA_HASH)           // consistent hashing
            .setFailureMode(FailureMode.Redistribute)                // redistribusi jika server mati
            .setOpTimeout(5000)                                       // timeout operasi 5 detik
            .setTimeoutExceptionThreshold(1998)
            .build()

        return MemcachedClient(factory, AddrUtil.getAddresses(
            System.getenv("MEMCACHED_SERVERS") ?: "localhost:11211"
        ))
    }

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

fun main() {
    val client = KoneksiMemcached.client
    client.set("test", 60, "Halo Memcached!")
    println(client.get("test"))  // Halo Memcached!
    client.shutdown()
}

Operasi Dasar #

Memcached hanya mendukung operasi sederhana — itulah poin kekuatannya:

fun operasiDasar() {
    val client = KoneksiMemcached.client
    val TTL = 300  // 5 menit dalam detik

    // SET — simpan nilai (menimpa jika sudah ada)
    client.set("nama", TTL, "Budi Santoso")

    // GET — ambil nilai
    val nama = client.get("nama") as String?
    println(nama)  // Budi Santoso

    // DELETE — hapus key
    client.delete("nama")
    println(client.get("nama"))  // null

    // ADD — simpan hanya jika key BELUM ADA
    val berhasil1 = client.add("kunci.unik", TTL, "nilai pertama").get()  // true
    val berhasil2 = client.add("kunci.unik", TTL, "nilai kedua").get()   // false (sudah ada)
    println("berhasil1: $berhasil1, berhasil2: $berhasil2")

    // REPLACE — perbarui hanya jika key SUDAH ADA
    client.set("kunci.ada", TTL, "lama")
    val updated = client.replace("kunci.ada", TTL, "baru").get()     // true
    val notFound = client.replace("kunci.tidak.ada", TTL, "nilai").get()  // false
    println("updated: $updated, notFound: $notFound")

    // APPEND dan PREPEND — tambahkan teks (tanpa TTL baru)
    client.set("log", TTL, "awal")
    client.append(0, "log", " | tengah")  // 0 = CAS tidak digunakan
    client.prepend(0, "log", "prefix | ")
    println(client.get("log") as String)  // prefix | awal | tengah

    // INCR dan DECR — untuk nilai numerik (nilai harus berupa string angka)
    client.set("pengunjung", TTL, "0")
    client.incr("pengunjung", 1)   // 1
    client.incr("pengunjung", 5)   // 6
    client.decr("pengunjung", 2)   // 4
    println(client.get("pengunjung"))  // 4

    // FLUSH ALL — hapus semua data (hati-hati di production!)
    // client.flush()
}

Serialisasi Objek #

Memcached hanya menyimpan byte array, tapi Spymemcached bisa menyimpan objek Java yang Serializable secara otomatis. Untuk Kotlin data class, gunakan serialisasi manual:

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.Serializable as JavaSerializable

// Cara 1: implementasikan java.io.Serializable
data class Pengguna(
    val id: Long,
    val nama: String,
    val email: String,
    val aktif: Boolean = true
) : JavaSerializable

// Cara 2: serialisasi manual dengan kotlinx.serialization (lebih fleksibel)
@kotlinx.serialization.Serializable
data class Produk(
    val id: Int,
    val nama: String,
    val harga: Double,
    val stok: Int
)

class CacheProduk {
    private val client = KoneksiMemcached.client
    private val json = Json { ignoreUnknownKeys = true }
    private val TTL = 300

    fun simpan(produk: Produk) {
        val key = "produk:${produk.id}"
        val nilai = json.encodeToString(Produk.serializer(), produk)
        client.set(key, TTL, nilai)
    }

    fun ambil(id: Int): Produk? {
        val key = "produk:$id"
        val nilai = client.get(key) as String? ?: return null
        return runCatching {
            json.decodeFromString(Produk.serializer(), nilai)
        }.getOrNull()
    }

    fun hapus(id: Int) {
        client.delete("produk:$id")
    }

    // Cache banyak produk — satu per satu (Memcached tidak punya MSET)
    fun simpanBanyak(produk: List<Produk>) {
        produk.forEach { p ->
            simpan(p)
        }
        println("${produk.size} produk di-cache")
    }
}

Multi-Get — Ambil Banyak Key Sekaligus #

Multi-get adalah salah satu operasi Memcached yang sangat efisien — satu network round-trip untuk banyak key:

fun contohMultiGet() {
    val client = KoneksiMemcached.client

    // Simpan beberapa produk
    val produk = mapOf(
        "produk:1" to """{"id":1,"nama":"Laptop","harga":15000000.0,"stok":10}""",
        "produk:2" to """{"id":2,"nama":"Mouse","harga":250000.0,"stok":50}""",
        "produk:3" to """{"id":3,"nama":"Keyboard","harga":500000.0,"stok":25}"""
    )
    produk.forEach { (key, nilai) -> client.set(key, 300, nilai) }

    // Multi-get: ambil semua dalam satu request
    val keys = listOf("produk:1", "produk:2", "produk:3", "produk:99")
    val hasil = client.getBulk(keys)

    println("Ditemukan ${hasil.size} dari ${keys.size} key:")
    hasil.forEach { (key, nilai) ->
        println("  $key = ${(nilai as String).take(40)}...")
    }
    // produk:99 tidak ada di hasil karena tidak ada di cache
}

CAS — Check-And-Set (Optimistic Locking) #

CAS mencegah race condition dengan memastikan nilai tidak berubah antara GET dan SET:

import net.spy.memcached.CASResponse
import net.spy.memcached.CASValue

fun contohCas() {
    val client = KoneksiMemcached.client
    val key = "stok:produk:1"

    client.set(key, 300, "100")

    // GETS — ambil nilai beserta CAS token
    val casValue: CASValue<Any> = client.gets(key)
    val stokSekarang = (casValue.value as String).toInt()
    val casToken = casValue.cas

    println("Stok: $stokSekarang, CAS: $casToken")

    // Simulasi: hitung stok baru
    val stokBaru = stokSekarang - 5

    // CAS — perbarui hanya jika nilai belum berubah sejak GETS
    val respons = client.cas(key, casToken, 300, stokBaru.toString())

    when (respons) {
        CASResponse.OK -> println("Stok berhasil diperbarui ke $stokBaru")
        CASResponse.EXISTS -> println("Gagal: nilai berubah sejak dibaca (race condition)")
        CASResponse.NOT_FOUND -> println("Key tidak ditemukan")
        else -> println("Respons tidak dikenal: $respons")
    }
}

// CAS dengan retry untuk menangani konflik
fun kurangiStokAman(key: String, jumlah: Int, maksPercobaan: Int = 3): Boolean {
    val client = KoneksiMemcached.client

    repeat(maksPercobaan) { percobaan ->
        val casValue = client.gets(key) ?: return false
        val stokSekarang = (casValue.value as String).toIntOrNull() ?: return false

        if (stokSekarang < jumlah) {
            println("Stok tidak cukup: $stokSekarang < $jumlah")
            return false
        }

        val stokBaru = stokSekarang - jumlah
        val respons = client.cas(key, casValue.cas, 300, stokBaru.toString())

        if (respons == CASResponse.OK) {
            println("Percobaan ${percobaan + 1}: stok berhasil dikurangi ke $stokBaru")
            return true
        }

        println("Percobaan ${percobaan + 1}: konflik CAS, coba lagi...")
        Thread.sleep(10)  // tunggu sejenak sebelum retry
    }

    println("Gagal setelah $maksPercobaan percobaan")
    return false
}

Pola Caching dengan Memcached #

Cache-Aside Pattern #

class LayananDataDenganCache(private val database: Database) {
    private val client = KoneksiMemcached.client
    private val json = Json { ignoreUnknownKeys = true }
    private val TTL = 600  // 10 menit

    fun ambilPengguna(id: Long): Pengguna? {
        val key = "pengguna:$id"

        // 1. Cek cache
        val cached = client.get(key) as String?
        if (cached != null) {
            return json.decodeFromString(Pengguna.serializer(), cached)
        }

        // 2. Cache miss — ambil dari DB
        val pengguna = database.cariPenggunaById(id) ?: return null

        // 3. Simpan ke cache
        client.set(key, TTL, json.encodeToString(Pengguna.serializer(), pengguna))
        return pengguna
    }

    fun invalidasiPengguna(id: Long) {
        client.delete("pengguna:$id")
    }

    // Namespace pattern — invalidasi grup key
    fun buatKeyDenganNamespace(namespace: String, id: Any): String {
        // Ambil versi namespace dari Memcached
        val versiKey = "namespace:$namespace"
        val versi = (client.get(versiKey) as String?)?.toInt() ?: run {
            client.add(versiKey, 0, "1")  // TTL 0 = tidak expire
            1
        }
        return "$namespace:v$versi:$id"
    }

    fun invalidasiNamespace(namespace: String) {
        // Increment versi = semua key lama otomatis invalid
        client.incr("namespace:$namespace", 1)
        println("Namespace '$namespace' diinvalidasi — semua key lama tidak valid")
    }
}

Koneksi dengan XMemcached #

XMemcached adalah alternatif yang lebih modern dengan dukungan NIO dan operasi asinkron:

// build.gradle.kts
// implementation("com.googlecode.xmemcached:xmemcached:2.4.7")

import net.rubyeye.xmemcached.MemcachedClient
import net.rubyeye.xmemcached.XMemcachedClientBuilder
import net.rubyeye.xmemcached.utils.AddrUtil
import net.rubyeye.xmemcached.command.BinaryCommandFactory

fun buatXMemcachedClient(): MemcachedClient {
    val builder = XMemcachedClientBuilder(
        AddrUtil.getAddresses("localhost:11211")
    ).apply {
        commandFactory = BinaryCommandFactory()  // protokol binary
        connectionPoolSize = 2                    // pool koneksi per server
        setOpTimeout(5000)                        // timeout 5 detik
    }
    return builder.build()
}

fun contohXMemcached() {
    val client = buatXMemcachedClient()

    // Operasi dasar sama dengan Spymemcached
    client.set("key", 300, "nilai")
    val nilai = client.get<String>("key")
    println(nilai)  // nilai

    // Multi-get lebih type-safe
    client.set("a", 300, "nilai A")
    client.set("b", 300, "nilai B")
    val banyak = client.get<String>(listOf("a", "b", "c"))
    println(banyak)  // {a=nilai A, b=nilai B}

    // Counter
    client.set("counter", 300, 0.toString())
    client.incr("counter", 1)
    client.incr("counter", 1)
    println(client.get<String>("counter"))  // 2

    client.shutdown()
}

Distributed Caching — Consistent Hashing #

Memcached mendistribusikan data ke banyak server menggunakan consistent hashing. Saat server ditambah atau dihapus, hanya sebagian kecil key yang perlu dipindah:

// Semua operasi sama — client yang menentukan server mana yang menyimpan key
fun contohDistributed() {
    // Buat client dengan 3 server
    val client = KoneksiMemcached.buatClientDistributed(
        listOf("server1:11211", "server2:11211", "server3:11211")
    )

    // Client otomatis mendistribusikan ke server yang tepat berdasarkan hash key
    client.set("pengguna:1", 300, "Budi")   // → server1 (misalnya)
    client.set("pengguna:2", 300, "Sari")   // → server3 (misalnya)
    client.set("pengguna:3", 300, "Ahmad")  // → server2 (misalnya)

    // GET juga otomatis diarahkan ke server yang benar
    println(client.get("pengguna:1"))  // Budi
    println(client.get("pengguna:2"))  // Sari

    client.shutdown()
}

/*
 Visualisasi consistent hashing:
 
 Server 1 ●────────────────────────● Server 2
          │         Ring            │
 Server 3 ●────────────────────────●
          
 Key di-hash ke posisi dalam ring, lalu ditempatkan di server
 searah jarum jam yang paling dekat.
 
 Saat server ditambah/dihapus → hanya ~K/N key yang perlu dipindah
 (K = jumlah key, N = jumlah server)
*/

Perbandingan Spymemcached vs XMemcached #

AspekSpymemcachedXMemcached
StatusMaintenance modeAktif dikembangkan
ThreadingAsync, satu thread I/ONIO, lebih skalabel
APIFuture untuk asyncSync dan async
ProtocolText dan BinaryText dan Binary
PopularitasSangat luasLebih populer di Asia
Integrasi Spring✓ Spring Cache✓ Spring Cache
DokumentasiBaikBaik

Tips Produksi #

// 1. Jangan simpan objek Java besar — Memcached maks 1MB per item by default
//    Compress jika perlu:
import java.util.zip.GZIPOutputStream
import java.util.zip.GZIPInputStream
import java.io.ByteArrayOutputStream
import java.io.ByteArrayInputStream

fun kompres(data: String): ByteArray {
    val bos = ByteArrayOutputStream()
    GZIPOutputStream(bos).use { it.write(data.toByteArray()) }
    return bos.toByteArray()
}

fun dekompress(data: ByteArray): String {
    return GZIPInputStream(ByteArrayInputStream(data)).bufferedReader().readText()
}

// 2. Gunakan konsisten dalam penamaan key:
//    Format: {entity}:{id}:{field_opsional}
//    Contoh: pengguna:1001, produk:5:harga, sesi:tok_abc123

// 3. Hindari key yang terlalu panjang — Memcached maks 250 karakter per key
//    Jika key panjang, hash dengan MD5/SHA

// 4. Set TTL yang tepat:
//    - Data yang jarang berubah: TTL panjang (jam - hari)
//    - Data yang sering berubah: TTL pendek (detik - menit)
//    - Jangan pernah TTL = 0 untuk data caching (beda dengan Java Serializable null)

// 5. Monitor hit rate — target di atas 80%
//    stats perintah di Memcached:
//    telnet localhost 11211
//    > stats
//    cmd_get, get_hits, get_misses → hitung hit rate = get_hits / cmd_get

Ringkasan #

  • Memcached untuk caching murni yang sederhana — jika kebutuhanmu hanya menyimpan dan mengambil data dengan key, Memcached adalah pilihan yang lebih ringan dari Redis dan bisa di-scale horizontal dengan sangat mudah.
  • Consistent hashing untuk distribusi — gunakan KETAMA_HASH di Spymemcached untuk distribusi yang merata dan minimal redistribusi saat node berubah. Client yang mengurus routing, kamu tidak perlu melakukan apapun.
  • CAS untuk concurrency amangets() + cas() adalah cara atomik untuk update nilai yang mungkin diakses bersamaan. Jika ada konflik (nilai berubah sejak dibaca), retry dengan backoff.
  • Multi-get untuk efisiensigetBulk(keys) mengirim satu request untuk banyak key, jauh lebih efisien dari loop get() satu per satu.
  • TTL selalu dibutuhkan — berbeda dari Redis yang bisa punya key tanpa expire, Memcached menggunakan LRU eviction. Set TTL yang masuk akal untuk semua key agar data lama tidak memakan memori.
  • Namespace versioning untuk invalidasi massal — daripada menghapus ratusan key satu per satu, gunakan pola namespace dengan increment versi. Semua key versi lama otomatis “invalid” tanpa benar-benar dihapus.
  • Spymemcached untuk kompatibilitas, XMemcached untuk performa — Spymemcached masih dominan di banyak proyek legacy. XMemcached lebih aktif dikembangkan dan lebih efisien untuk throughput tinggi.
  • Kompres data besar — Memcached membatasi ukuran item hingga 1MB by default. Untuk payload yang lebih besar, kompres dengan GZIP sebelum disimpan dan decompress saat diambil.

← Sebelumnya: Redis   Berikutnya: Quarkus →

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