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| Aspek | Memcached | Redis |
|---|---|---|
| Tipe data | Key-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 |
| Threading | Multi-thread | Single-thread (Redis 6+ multi-IO) |
| Overhead memori | Sangat rendah | Sedikit lebih tinggi |
| Pub/Sub | ✗ Tidak ada | ✓ Ada |
| Atomic operations | CAS, append | INCR, DECR, GETSET, banyak lagi |
| Cocok untuk | Pure caching, simple key-value | Caching + 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 #
| Aspek | Spymemcached | XMemcached |
|---|---|---|
| Status | Maintenance mode | Aktif dikembangkan |
| Threading | Async, satu thread I/O | NIO, lebih skalabel |
| API | Future untuk async | Sync dan async |
| Protocol | Text dan Binary | Text dan Binary |
| Popularitas | Sangat luas | Lebih populer di Asia |
| Integrasi Spring | ✓ Spring Cache | ✓ Spring Cache |
| Dokumentasi | Baik | Baik |
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_HASHdi Spymemcached untuk distribusi yang merata dan minimal redistribusi saat node berubah. Client yang mengurus routing, kamu tidak perlu melakukan apapun.- CAS untuk concurrency aman —
gets()+cas()adalah cara atomik untuk update nilai yang mungkin diakses bersamaan. Jika ada konflik (nilai berubah sejak dibaca), retry dengan backoff.- Multi-get untuk efisiensi —
getBulk(keys)mengirim satu request untuk banyak key, jauh lebih efisien dari loopget()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.