Memcached

Memcached #

Memcached adalah cache in-memory yang sangat sederhana dan sangat cepat. Ia sengaja dirancang minimalis: hanya mendukung satu tipe data (string/bytes), tidak ada persistensi, tidak ada replikasi bawaan, tidak ada struktur data kompleks. Kesederhanaannya adalah kekuatannya — overhead per operasi lebih rendah dari Redis, dan arsitektur multi-threaded-nya memanfaatkan CPU multi-core lebih efektif. Skala Memcached dilakukan secara horizontal dengan menambah server, dan client melakukan consistent hashing untuk menentukan server mana yang menyimpan setiap kunci. Di Rust, crate memcache menyediakan akses synchronous dan ada async-memcached untuk kebutuhan async. Artikel ini membahas semua operasi Memcached, pola caching yang idiomatis, dan kapan memilih Memcached dibanding Redis.

Memcached vs Redis — Pilih yang Tepat #

Sebelum masuk ke kode, penting memahami kapan Memcached lebih tepat dari Redis:

flowchart TD
    Q{Kebutuhan caching?}
    Q --> A["Hanya perlu cache\nstring/bytes sederhana\nThroughput maksimal\nCPU multi-core optimal"]
    Q --> B["Perlu struktur data\n(Hash, List, Set)\nPersistensi\nPub/Sub\nLua scripts"]
    Q --> C["Cluster terdistribusi\ndengan skalabilitas\nhorizontal linear"]

    A --> MC["Memcached\n✓ Lebih sederhana\n✓ Multi-threaded native\n✓ Overhead lebih rendah"]
    B --> RD["Redis\n✓ Lebih kaya fitur\n✓ Persistensi opsional\n✓ Operasi atomik kompleks"]
    C --> MC2["Memcached\n✓ Consistent hashing\n✓ Scale linear\n✓ Tanpa koordinasi antar node"]
AspekMemcachedRedis
Tipe dataHanya string/bytes10+ tipe (String, Hash, List, Set, ZSet, dll.)
PersistensiTidakOpsional (AOF/RDB)
ThreadingMulti-threadedSingle-threaded (I/O)
ClusteringClient-side shardingBuilt-in Cluster mode
ReplikasiTidak nativeBuilt-in
Operasi atomikCAS, INCR/DECRBanyak (MULTI, Lua, dll.)
MemoriLebih efisien per itemLebih banyak overhead
KompleksitasSangat rendahSedang
KapanCache sederhana, throughput tinggiFitur lengkap, persistensi

Instalasi #

[dependencies]
# Synchronous driver
memcache = "0.17"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

# Atau untuk async
# async-memcached = "0.4"
Crate memcache adalah synchronous. Untuk aplikasi async, jalankan operasi Memcached di dalam tokio::task::spawn_blocking agar tidak memblokir event loop, atau gunakan crate async-memcached yang lebih baru tapi masih berkembang.

Koneksi Dasar #

use memcache::Client;

fn buat_client(url: &str) -> Result<Client, memcache::MemcacheError> {
    // Koneksi ke satu server
    Client::connect(url)
}

fn buat_client_multi(urls: &[&str]) -> Result<Client, memcache::MemcacheError> {
    // Koneksi ke beberapa server — client melakukan consistent hashing
    Client::connect(urls.to_vec())
}

fn main() -> Result<(), memcache::MemcacheError> {
    // Format: memcache://host:port atau memcache://host:port?timeout=5&tcp_nodelay=true
    let client = buat_client("memcache://localhost:11211")?;

    // Ping untuk verifikasi
    client.version()?;
    println!("Terhubung ke Memcached");

    // Cluster: kunci didistribusikan ke server berdasarkan consistent hashing
    let cluster = buat_client_multi(&[
        "memcache://server1:11211",
        "memcache://server2:11211",
        "memcache://server3:11211",
    ])?;

    Ok(())
}

Operasi Dasar #

SET — Menyimpan Nilai #

use memcache::Client;

fn contoh_set(client: &Client) -> Result<(), memcache::MemcacheError> {
    // SET dengan expiry dalam detik (0 = tidak expire)
    client.set("nama", "Budi Santoso", 3600)?;  // expire 1 jam
    client.set("counter", 42u64, 0)?;            // tidak expire
    client.set("aktif", true, 600)?;             // expire 10 menit

    // SET dengan bytes mentah
    let data = b"binary data";
    client.set("raw", data.as_ref(), 300)?;

    // SET dengan struct yang di-serialisasi ke JSON
    let config = serde_json::json!({
        "host": "localhost",
        "port": 8080,
        "debug": true
    });
    client.set("config", config.to_string().as_str(), 3600)?;

    println!("SET berhasil");
    Ok(())
}

GET — Mengambil Nilai #

fn contoh_get(client: &Client) -> Result<(), memcache::MemcacheError> {
    // GET string
    let nama: Option<String> = client.get("nama")?;
    match nama {
        Some(n) => println!("Nama: {}", n),
        None    => println!("Kunci tidak ditemukan atau sudah expire"),
    }

    // GET dengan tipe yang berbeda
    let counter: Option<u64> = client.get("counter")?;
    println!("Counter: {:?}", counter);

    // GET JSON dan parse ke struct
    if let Some(json_str): Option<String> = client.get("config")? {
        let config: serde_json::Value = serde_json::from_str(&json_str)
            .unwrap_or_default();
        println!("Port: {}", config["port"]);
    }

    // GET banyak kunci sekaligus
    let banyak: std::collections::HashMap<String, String> = client.gets(
        &["nama", "config", "kunci_tidak_ada"]
    )?;
    println!("Multi GET: {} kunci ditemukan", banyak.len());
    for (k, v) in &banyak {
        println!("  {}: {}...", k, &v[..v.len().min(30)]);
    }

    Ok(())
}

ADD, REPLACE, dan DELETE #

fn contoh_lainnya(client: &Client) -> Result<(), memcache::MemcacheError> {
    // ADD — set HANYA jika kunci belum ada (seperti SET NX di Redis)
    let berhasil = client.add("lock:resource", "owner-1", 30)?;
    println!("ADD berhasil: {}", berhasil);  // true jika berhasil

    // ADD lagi pada kunci yang sama → gagal (sudah ada)
    let gagal = client.add("lock:resource", "owner-2", 30)?;
    println!("ADD kedua (harus gagal): {}", gagal);  // false

    // REPLACE — set HANYA jika kunci sudah ada
    client.set("existing", "nilai-awal", 300)?;
    let berhasil_replace = client.replace("existing", "nilai-baru", 300)?;
    println!("REPLACE berhasil: {}", berhasil_replace);  // true

    let gagal_replace = client.replace("tidak-ada", "nilai", 300)?;
    println!("REPLACE kunci tidak ada (harus gagal): {}", gagal_replace);  // false

    // DELETE — hapus kunci
    client.delete("nama")?;
    println!("Kunci 'nama' dihapus");

    // DELETE kunci yang tidak ada — tidak error
    client.delete("kunci-tidak-ada")?;

    // FLUSH ALL — hapus semua kunci (hati-hati di produksi!)
    // client.flush()?;  // uncomment untuk flush semua

    Ok(())
}

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

CAS memungkinkan update atomik: ambil nilai bersama token unik (CAS token), lakukan modifikasi, lalu set kembali — tapi hanya jika token masih valid (tidak ada yang mengubah nilai di antaranya):

fn contoh_cas(client: &Client) -> Result<(), memcache::MemcacheError> {
    // Simpan nilai awal
    client.set("saldo", "1000000", 3600)?;

    // GETS — ambil nilai bersama CAS token
    let hasil: Option<(Vec<u8>, u64)> = client.gets_cas("saldo")?;

    if let Some((data, cas_token)) = hasil {
        let saldo_str = String::from_utf8_lossy(&data);
        let saldo: u64 = saldo_str.parse().unwrap_or(0);
        println!("Saldo saat ini: {}, CAS token: {}", saldo, cas_token);

        let saldo_baru = saldo - 50_000;  // simulasi debit

        // CAS — set hanya jika token masih valid
        // Jika ada transaksi lain yang mengubah saldo di antara GETS dan CAS ini,
        // operasi akan gagal dan perlu diulang
        let berhasil = client.cas("saldo", &saldo_baru.to_string(), 3600, cas_token)?;

        if berhasil {
            println!("Saldo berhasil diperbarui: {}", saldo_baru);
        } else {
            println!("CAS gagal — saldo berubah oleh proses lain, coba lagi");
        }
    }

    Ok(())
}

// CAS dengan retry loop — pola umum optimistic locking
fn perbarui_dengan_cas(
    client: &Client,
    kunci: &str,
    transform: impl Fn(u64) -> u64,
    maks_retry: u32,
) -> Result<u64, String> {
    for percobaan in 0..maks_retry {
        let hasil: Option<(Vec<u8>, u64)> = client.gets_cas(kunci)
            .map_err(|e| e.to_string())?;

        match hasil {
            None => return Err(format!("Kunci '{}' tidak ditemukan", kunci)),
            Some((data, cas_token)) => {
                let nilai: u64 = String::from_utf8_lossy(&data)
                    .parse()
                    .unwrap_or(0);
                let nilai_baru = transform(nilai);

                let berhasil = client.cas(kunci, &nilai_baru.to_string(), 3600, cas_token)
                    .map_err(|e| e.to_string())?;

                if berhasil {
                    return Ok(nilai_baru);
                }
                println!("Percobaan {} gagal, mencoba lagi...", percobaan + 1);
            }
        }
    }
    Err(format!("Gagal setelah {} percobaan", maks_retry))
}

Increment dan Decrement Atomik #

fn contoh_incr_decr(client: &Client) -> Result<(), memcache::MemcacheError> {
    // Set nilai awal sebagai string angka
    client.set("halaman_views", "0", 0)?;
    client.set("slot_tersedia", "100", 0)?;

    // INCREMENT — tambah secara atomik
    let views: u64 = client.increment("halaman_views", 1)?;
    println!("Views: {}", views);  // 1

    let views_multi: u64 = client.increment("halaman_views", 10)?;
    println!("Views setelah +10: {}", views_multi);  // 11

    // DECREMENT — kurangi secara atomik
    let slot: u64 = client.decrement("slot_tersedia", 1)?;
    println!("Slot tersedia: {}", slot);  // 99

    // DECREMENT tidak bisa negatif — berhenti di 0
    client.set("kecil", "2", 0)?;
    let _ = client.decrement("kecil", 10)?;  // menjadi 0, bukan -8
    let nilai: Option<u64> = client.get("kecil")?;
    println!("Nilai minimum 0: {:?}", nilai);

    Ok(())
}

Pola Caching dengan Async via spawn_blocking #

use std::sync::Arc;
use memcache::Client;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
struct DaftarProduk {
    pub items: Vec<String>,
    pub total: u64,
    pub halaman: u32,
}

// Bungkus Client dalam Arc untuk shared ownership
type McClient = Arc<Client>;

async fn ambil_daftar_produk(
    mc: McClient,
    halaman: u32,
) -> Result<DaftarProduk, Box<dyn std::error::Error + Send + Sync>> {
    let kunci = format!("produk:halaman:{}", halaman);

    // Jalankan operasi Memcached di thread pool agar tidak blokir async runtime
    let mc_clone = Arc::clone(&mc);
    let kunci_clone = kunci.clone();

    let cached = tokio::task::spawn_blocking(move || {
        mc_clone.get::<String>(&kunci_clone)
    })
    .await??;

    if let Some(json_str) = cached {
        println!("Cache HIT: halaman {}", halaman);
        let daftar: DaftarProduk = serde_json::from_str(&json_str)?;
        return Ok(daftar);
    }

    // Cache MISS — ambil dari database
    println!("Cache MISS: halaman {}", halaman);
    let daftar = DaftarProduk {
        items: (1..=10).map(|i| format!("Produk {}", i + (halaman - 1) * 10)).collect(),
        total: 100,
        halaman,
    };

    // Simpan ke cache
    let json_str = serde_json::to_string(&daftar)?;
    let mc_store = Arc::clone(&mc);
    tokio::task::spawn_blocking(move || {
        mc_store.set(&kunci, json_str.as_str(), 300)
    })
    .await??;

    Ok(daftar)
}

// Wrapper generik untuk pola cache-aside
async fn dengan_cache<T, F, Fut>(
    mc: McClient,
    kunci: &str,
    ttl: u32,
    fetch: F,
) -> Result<T, Box<dyn std::error::Error + Send + Sync>>
where
    T: serde::Serialize + for<'de> serde::Deserialize<'de> + Send + 'static,
    F: FnOnce() -> Fut,
    Fut: std::future::Future<Output = Result<T, Box<dyn std::error::Error + Send + Sync>>>,
{
    let mc_get = Arc::clone(&mc);
    let kunci_str = kunci.to_string();

    // Cek cache
    let cached: Option<String> = tokio::task::spawn_blocking(move || {
        mc_get.get::<String>(&kunci_str)
    })
    .await??;

    if let Some(json) = cached {
        if let Ok(nilai) = serde_json::from_str::<T>(&json) {
            return Ok(nilai);
        }
    }

    // Ambil dari sumber
    let nilai = fetch().await?;

    // Simpan ke cache
    let json = serde_json::to_string(&nilai)?;
    let mc_set = Arc::clone(&mc);
    let kunci_set = kunci.to_string();
    tokio::task::spawn_blocking(move || {
        mc_set.set(&kunci_set, json.as_str(), ttl)
    })
    .await??;

    Ok(nilai)
}

Distributed Caching dengan Consistent Hashing #

Memcached mendistribusikan kunci ke beberapa server secara otomatis di sisi client:

fn contoh_distribusi(client: &Client) -> Result<(), memcache::MemcacheError> {
    // Client secara otomatis mendistribusikan kunci ke server yang berbeda
    // berdasarkan consistent hashing dari nama kunci

    // Kunci-kunci ini mungkin tersimpan di server yang berbeda
    for i in 0..20 {
        let kunci = format!("item:{}", i);
        let nilai = format!("nilai-{}", i);
        client.set(&kunci, nilai.as_str(), 300)?;
    }

    // Baca semua — client tahu ke server mana harus bertanya untuk setiap kunci
    for i in 0..20 {
        let kunci = format!("item:{}", i);
        let nilai: Option<String> = client.get(&kunci)?;
        println!("{}: {:?}", kunci, nilai);
    }

    // Statistik server — info per server
    let stats = client.stats()?;
    for (server, stat_map) in stats {
        println!("Server: {}", server);
        if let Some(bytes) = stat_map.get("bytes") {
            println!("  Memory used: {} bytes", bytes);
        }
        if let Some(curr_items) = stat_map.get("curr_items") {
            println!("  Current items: {}", curr_items);
        }
        if let Some(hits) = stat_map.get("get_hits") {
            println!("  Cache hits: {}", hits);
        }
        if let Some(misses) = stat_map.get("get_misses") {
            println!("  Cache misses: {}", misses);
        }
    }

    Ok(())
}

Strategi Invalidasi Cache #

fn strategi_invalidasi(client: &Client) -> Result<(), memcache::MemcacheError> {
    // 1. TTL-based: otomatis expire setelah N detik
    client.set("data:ttl", "nilai", 300)?;

    // 2. Explicit delete: hapus saat data berubah
    client.delete("data:produk:1001")?;

    // 3. Namespace versioning: ganti versi saat invalidasi massal
    // Daripada menghapus ribuan kunci satu per satu, ganti versi namespace
    client.set("ns:produk:v", "2", 0)?;  // versi baru = 2

    // Kunci baru menggunakan versi terbaru
    let versi: Option<String> = client.get("ns:produk:v")?;
    let v = versi.as_deref().unwrap_or("1");
    let kunci_produk = format!("produk:v{}:1001", v);
    client.set(&kunci_produk, "data-produk-baru", 300)?;

    // Kunci dengan versi lama tidak akan ditemukan (sudah ganti namespace)
    // Cara ini menghindari thundering herd dan cache stampede

    // 4. Tag-based invalidasi — simpan daftar kunci per tag
    // (Memcached tidak support native, implementasi manual via kunci khusus)
    client.set("tags:kategori:elektronik", "key1,key2,key3", 0)?;

    Ok(())
}

Kapan Memilih Memcached vs Redis #

Gunakan Memcached jika:
  ✓ Hanya butuh caching nilai sederhana (string/bytes/JSON)
  ✓ Throughput tertinggi dan latensi terendah adalah prioritas
  ✓ Skala horizontal linear tanpa koordinasi antar node
  ✓ Tidak butuh persistensi — cache boleh hilang saat restart
  ✓ Tim sudah familiar dengan model sederhana key-value

Gunakan Redis jika:
  ✓ Butuh struktur data (Hash, List, Sorted Set untuk leaderboard)
  ✓ Butuh persistensi (data tidak boleh hilang saat restart)
  ✓ Butuh Pub/Sub untuk notifikasi real-time
  ✓ Distributed lock yang andal dengan Lua script
  ✓ Rate limiting dengan sliding window (Sorted Set)
  ✓ Session store yang perlu diquery berdasarkan field
  ✓ Replikasi dan failover otomatis (Redis Sentinel/Cluster)

Ringkasan #

  • Memcached hanya mendukung string/bytes — tidak ada hash, list, atau sorted set seperti Redis. Untuk struktur data, serialisasi ke JSON dan simpan sebagai string.
  • CAS untuk optimistic lockinggets_cas mengambil nilai dan token, cas mengupdate hanya jika token masih valid. Ini cara Memcached mencegah race condition tanpa blocking.
  • add untuk lock sederhanaadd gagal jika kunci sudah ada, setara dengan SET NX di Redis. Kombinasikan dengan TTL untuk distributed lock tanpa CAS.
  • increment/decrement atomik — tidak bisa negatif (berhenti di 0). Gunakan untuk counter yang tidak boleh negative.
  • Bungkus dalam spawn_blocking untuk async — crate memcache synchronous; jalankan di thread pool untuk tidak memblokir tokio runtime.
  • Consistent hashing di sisi client — tidak ada koordinasi antar server Memcached. Saat server ditambah atau dihapus, persentase kunci yang perlu di-rehash minimal.
  • Namespace versioning untuk invalidasi massal — ubah versi namespace daripada hapus ribuan kunci satu per satu. Mencegah cache stampede saat invalidasi besar-besaran.
  • Monitor dengan client.stats() — pantau hit rate, miss rate, dan penggunaan memori per server untuk tuning yang tepat.
  • Pilih Redis untuk fitur lengkap, Memcached untuk kesederhanaan dan throughput — jika ragu, mulai dengan Redis karena lebih fleksibel; pindah ke Memcached hanya jika ada bukti bottleneck yang jelas.

← Sebelumnya: Redis   Berikutnya: Actix →

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