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"]| Aspek | Memcached | Redis |
|---|---|---|
| Tipe data | Hanya string/bytes | 10+ tipe (String, Hash, List, Set, ZSet, dll.) |
| Persistensi | Tidak | Opsional (AOF/RDB) |
| Threading | Multi-threaded | Single-threaded (I/O) |
| Clustering | Client-side sharding | Built-in Cluster mode |
| Replikasi | Tidak native | Built-in |
| Operasi atomik | CAS, INCR/DECR | Banyak (MULTI, Lua, dll.) |
| Memori | Lebih efisien per item | Lebih banyak overhead |
| Kompleksitas | Sangat rendah | Sedang |
| Kapan | Cache sederhana, throughput tinggi | Fitur 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"
Cratememcacheadalah synchronous. Untuk aplikasi async, jalankan operasi Memcached di dalamtokio::task::spawn_blockingagar tidak memblokir event loop, atau gunakan crateasync-memcachedyang 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 locking —
gets_casmengambil nilai dan token,casmengupdate hanya jika token masih valid. Ini cara Memcached mencegah race condition tanpa blocking.adduntuk lock sederhana —addgagal jika kunci sudah ada, setara denganSET NXdi Redis. Kombinasikan dengan TTL untuk distributed lock tanpa CAS.increment/decrementatomik — tidak bisa negatif (berhenti di 0). Gunakan untuk counter yang tidak boleh negative.- Bungkus dalam
spawn_blockinguntuk async — cratememcachesynchronous; 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.