Memcached #
Memcached adalah distributed caching system yang sangat sederhana dan sangat cepat — dirancang untuk satu tujuan: menyimpan data di memori agar bisa diambil kembali jauh lebih cepat dari database. Berbeda dari Redis yang punya banyak struktur data dan fitur, Memcached hanya menyimpan key-value sederhana. Kesederhanaan inilah yang menjadi kekuatannya di skenario tertentu: latensi sangat rendah, throughput sangat tinggi, dan mudah di-scale horizontal dengan menambah node. PHP memiliki dua ekstensi untuk Memcached: Memcached (lebih baru, lebih kaya fitur, menggunakan libmemcached) dan Memcache (lebih lama, tidak disarankan untuk kode baru). Artikel ini fokus pada ekstensi Memcached yang merupakan pilihan modern.
Memcached vs Redis — Kapan Mana #
| Aspek | Memcached | Redis |
|---|---|---|
| Tipe data | Hanya string/serialized | String, Hash, List, Set, Sorted Set, dll. |
| Persistensi data | ✗ Tidak ada | ✓ RDB dan AOF |
| Clustering | ✓ Client-side sharding | ✓ Redis Cluster native |
| Pub/Sub | ✗ | ✓ |
| Lua scripting | ✗ | ✓ |
| Multi-threading | ✓ Native | ✓ (Redis 6+) |
| Memori overhead | Lebih rendah | Sedikit lebih tinggi |
| Use case terbaik | Cache murni, high throughput | Cache + fitur data structure |
Pilih Memcached jika:
✓ Hanya butuh cache sederhana key-value
✓ Throughput sangat tinggi dan latensi sangat rendah adalah prioritas
✓ Sudah ada infrastruktur Memcached yang berjalan
✓ Tidak butuh persistensi data sama sekali
Pilih Redis jika:
✓ Butuh struktur data selain key-value (list, set, sorted set)
✓ Butuh pub/sub, job queue, atau rate limiting
✓ Ingin data bertahan saat restart
✓ Memulai proyek baru — Redis lebih serbaguna
Instalasi #
# Install Memcached server
sudo apt install memcached
sudo systemctl enable memcached
sudo systemctl start memcached
# Install ekstensi PHP Memcached
sudo apt install php8.3-memcached
# Atau via PECL
sudo apt install libmemcached-dev
sudo pecl install memcached
echo "extension=memcached.so" | sudo tee /etc/php/8.3/mods-available/memcached.ini
sudo phpenmod memcached
# Verifikasi
php -m | grep memcached
memcached -h | head -5
Koneksi dan Server Pool #
Kekuatan Memcached adalah distribusi data ke beberapa server secara otomatis (consistent hashing):
<?php
$mc = new Memcached();
// Tambah satu server
$mc->addServer('127.0.0.1', 11211);
// Tambah beberapa server — Memcached mendistribusikan kunci secara otomatis
$mc->addServers([
['cache1.internal', 11211, 40], // host, port, bobot
['cache2.internal', 11211, 30],
['cache3.internal', 11211, 30],
]);
// Kunci didistribusikan berdasarkan bobot:
// cache1 dapat ~40% kunci, cache2 dan cache3 masing-masing ~30%
// Konfigurasi opsi penting
$mc->setOption(Memcached::OPT_COMPRESSION, true); // kompres nilai besar
$mc->setOption(Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP); // serializer default
$mc->setOption(Memcached::OPT_PREFIX_KEY, 'myapp:'); // prefix semua kunci
$mc->setOption(Memcached::OPT_CONNECT_TIMEOUT, 500); // timeout koneksi 500ms
$mc->setOption(Memcached::OPT_RECV_TIMEOUT, 1000); // timeout receive 1000ms
$mc->setOption(Memcached::OPT_SEND_TIMEOUT, 1000); // timeout send 1000ms
$mc->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true); // consistent hashing
$mc->setOption(Memcached::OPT_NO_BLOCK, true); // non-blocking I/O
// Cek status server
$stats = $mc->getStats();
foreach ($stats as $server => $info) {
echo "$server: " . $info['bytes_used'] . " bytes used, " . $info['curr_items'] . " items\n";
}
// Persistent connection — koneksi dipertahankan antar request
$mcPersisten = new Memcached('connection-pool-id');
// Hanya tambah server jika belum ada (persistent connection sudah ada server-nya)
if (!$mcPersisten->getServerList()) {
$mcPersisten->addServers([
['127.0.0.1', 11211],
]);
}
Operasi Dasar #
<?php
$mc = new Memcached();
$mc->addServer('127.0.0.1', 11211);
// SET — simpan nilai
$mc->set('kunci', 'nilai', 3600); // kunci, nilai, TTL dalam detik
$mc->set('user:42', ['nama' => 'Budi', 'email' => '[email protected]'], 1800);
// TTL = 0 berarti tidak expired (tapi bisa dihapus jika Memcached kehabisan memori)
$mc->set('config:app', ['debug' => false, 'version' => '2.0'], 0);
// GET — ambil nilai
$nilai = $mc->get('kunci');
if ($mc->getResultCode() === Memcached::RES_NOTFOUND) {
echo "Kunci tidak ditemukan\n";
// ambil dari database
}
$user = $mc->get('user:42');
if ($user !== false) {
echo $user['nama'] . "\n"; // Budi
}
// ADD — hanya simpan jika kunci belum ada
$berhasil = $mc->add('lock:proses', '1', 30);
if (!$berhasil) {
// kunci sudah ada — ada proses lain yang sedang berjalan
throw new \RuntimeException("Proses sedang berjalan");
}
// REPLACE — hanya update jika kunci sudah ada
$berhasil = $mc->replace('user:42', ['nama' => 'Budi Santoso'], 1800);
if (!$berhasil) {
// kunci tidak ada — perlu set dulu
$mc->set('user:42', ['nama' => 'Budi Santoso'], 1800);
}
// DELETE — hapus kunci
$mc->delete('kunci');
$mc->delete('kunci', 0); // hapus segera (delay = 0 detik)
// FLUSH — hapus semua kunci (gunakan dengan sangat hati-hati di production!)
// $mc->flush(); // BAHAYA: menghapus SEMUA cache di server
// INCREMENT / DECREMENT
$mc->set('counter:views', 0, 0);
$mc->increment('counter:views'); // +1, return nilai baru
$mc->increment('counter:views', 5); // +5
$mc->decrement('counter:views', 2); // -2
echo $mc->get('counter:views'); // 4
// increment / decrement dengan nilai awal jika kunci tidak ada
$mc->increment('counter:new', 1, 0, 3600); // +1, default 0, TTL 3600
// GETMULTI — ambil banyak kunci sekaligus
$nilai = $mc->getMulti(['kunci1', 'kunci2', 'user:42', 'tidak:ada']);
// ['kunci1' => 'nilai1', 'kunci2' => 'nilai2', 'user:42' => [...]]
// kunci yang tidak ada tidak ada dalam hasil
// Cara mendeteksi miss di getMulti
$keys = ['produk:1', 'produk:2', 'produk:3', 'produk:999'];
$cached = $mc->getMulti($keys);
$missed = array_diff($keys, array_keys($cached ?? []));
// $missed berisi kunci yang tidak ada di cache
// SETMULTI — simpan banyak kunci sekaligus
$mc->setMulti([
'a' => 'nilai-a',
'b' => 'nilai-b',
'c' => 'nilai-c',
], 3600);
// DELETEMULTI — hapus banyak kunci
$mc->deleteMulti(['a', 'b', 'c']);
CAS — Check and Set (Atomic Update) #
CAS memungkinkan update yang aman tanpa race condition — seperti optimistic locking:
<?php
// Skenario: dua request mencoba update counter yang sama bersamaan
// Langkah 1: ambil nilai DENGAN token CAS
$casToken = null;
$nilai = $mc->get('counter:stok:42', null, $casToken);
// $casToken sekarang berisi token unik untuk versi data ini
if ($nilai === false) {
// Tidak ada di cache — set dari awal
$mc->set('counter:stok:42', 10, 3600);
} else {
// Langkah 2: update HANYA jika data tidak berubah sejak dibaca
$berhasil = $mc->cas($casToken, 'counter:stok:42', $nilai - 1, 3600);
if (!$berhasil) {
if ($mc->getResultCode() === Memcached::RES_DATA_EXISTS) {
// Data sudah diubah oleh request lain sejak kita baca — coba lagi
echo "Konflik CAS — coba lagi\n";
// Implementasikan retry loop
}
}
}
// Contoh: update inventory dengan CAS dan retry
function kurangiStok(Memcached $mc, int $produkId, int $jumlah, int $maxRetry = 3): bool
{
$kunci = "stok:$produkId";
for ($percobaan = 0; $percobaan < $maxRetry; $percobaan++) {
$casToken = null;
$stok = $mc->get($kunci, null, $casToken);
if ($stok === false) {
return false; // stok tidak ada di cache
}
if ($stok < $jumlah) {
throw new \DomainException("Stok tidak mencukupi: $stok < $jumlah");
}
$berhasil = $mc->cas($casToken, $kunci, $stok - $jumlah, 3600);
if ($berhasil) {
return true; // berhasil!
}
if ($mc->getResultCode() !== Memcached::RES_DATA_EXISTS) {
return false; // error lain
}
// DATA_EXISTS — coba lagi
usleep(rand(1000, 5000)); // tunggu 1-5ms random sebelum retry
}
return false; // gagal setelah maxRetry percobaan
}
Namespace dan Invalidasi Grup Kunci #
Memcached tidak mendukung wildcard delete (seperti DEL produk:*) — salah satu keterbatasan terbesarnya. Trik umum untuk mengatasi ini adalah namespace versioning:
<?php
class MemcachedNamespace
{
public function __construct(private Memcached $mc) {}
// Dapatkan versi namespace saat ini
private function getVersion(string $namespace): int
{
$kunci = "ns_version:$namespace";
$versi = $this->mc->get($kunci);
if ($versi === false) {
$versi = 1;
$this->mc->set($kunci, $versi, 0); // tidak expired
}
return (int) $versi;
}
// Buat kunci dengan namespace versioned
public function kunci(string $namespace, string $kunci): string
{
$versi = $this->getVersion($namespace);
return "{$namespace}:v{$versi}:{$kunci}";
}
public function set(string $namespace, string $kunci, mixed $nilai, int $ttl = 3600): bool
{
return $this->mc->set($this->kunci($namespace, $kunci), $nilai, $ttl);
}
public function get(string $namespace, string $kunci): mixed
{
return $this->mc->get($this->kunci($namespace, $kunci));
}
// Invalidasi seluruh namespace — cukup increment versi!
// Semua kunci lama otomatis "tidak terlihat" karena versi berbeda
public function invalidasiNamespace(string $namespace): void
{
$kunci = "ns_version:$namespace";
// Jika belum ada, set dulu
if ($this->mc->get($kunci) === false) {
$this->mc->set($kunci, 2, 0);
} else {
$this->mc->increment($kunci); // increment versi
}
// Kunci lama masih ada di memori tapi tidak akan pernah diakses lagi
// Memcached akan menghapusnya saat LRU eviction atau TTL expired
}
}
// Penggunaan
$cache = new MemcachedNamespace($mc);
// Simpan data produk di namespace 'produk'
$cache->set('produk', '42', ['nama' => 'Laptop', 'harga' => 15000000]);
$cache->set('produk', '43', ['nama' => 'Monitor', 'harga' => 5000000]);
// Baca
$laptop = $cache->get('produk', '42');
// Invalidasi SEMUA kunci produk setelah update massal
$cache->invalidasiNamespace('produk');
// Sekarang semua cache produk "tidak ada" — akan di-refresh dari database
$laptop = $cache->get('produk', '42'); // null — miss!
Pola Caching yang Benar #
<?php
class CacheLayer
{
public function __construct(private Memcached $mc, private PDO $db) {}
// Cache-aside: baca dari cache, miss ambil dari DB
public function getProduk(int $id): ?array
{
$kunci = "produk:detail:$id";
$data = $this->mc->get($kunci);
if ($data !== false) {
return $data; // cache hit
}
// Cache miss — ambil dari database
$stmt = $this->db->prepare("SELECT * FROM produk WHERE id = :id AND aktif = 1");
$stmt->execute([':id' => $id]);
$produk = $stmt->fetch() ?: null;
if ($produk !== null) {
// Simpan ke cache — TTL 1 jam dengan sedikit jitter untuk mencegah stampede
$ttl = 3600 + random_int(-300, 300); // 55-65 menit
$this->mc->set($kunci, $produk, $ttl);
}
return $produk;
}
// Invalidasi setelah update
public function updateProduk(int $id, array $data): void
{
$stmt = $this->db->prepare("UPDATE produk SET nama = :nama, harga = :harga WHERE id = :id");
$stmt->execute([':nama' => $data['nama'], ':harga' => $data['harga'], ':id' => $id]);
// Hapus cache — akan di-refresh saat request berikutnya
$this->mc->delete("produk:detail:$id");
}
// Cache stampede prevention — dogpile lock
public function getProdukAntiStampede(int $id): ?array
{
$kunci = "produk:detail:$id";
$kunciLock = "produk:lock:$id";
$data = $this->mc->get($kunci);
if ($data !== false) {
return $data;
}
// Coba ambil lock — hanya satu request yang boleh hit DB
$mendapatLock = $this->mc->add($kunciLock, '1', 10); // lock 10 detik
if (!$mendapatLock) {
// Request lain sedang fetch — tunggu sebentar dan coba cache lagi
usleep(200_000); // 200ms
$data = $this->mc->get($kunci);
return $data !== false ? $data : null;
}
try {
$stmt = $this->db->prepare("SELECT * FROM produk WHERE id = :id");
$stmt->execute([':id' => $id]);
$produk = $stmt->fetch() ?: null;
if ($produk !== null) {
$this->mc->set($kunci, $produk, 3600);
}
return $produk;
} finally {
$this->mc->delete($kunciLock); // selalu lepas lock
}
}
}
Anti-Pattern Memcached yang Sering Ditemui #
<?php
// ✗ Anti-pattern 1: menyimpan terlalu banyak data dalam satu kunci
$mc->set('semua:produk', $db->query("SELECT * FROM produk")->fetchAll()); // ribuan produk!
// Memcached punya batas 1MB per item secara default
// ✓ Cache per item atau per halaman
$mc->set("produk:halaman:1", $produkHalaman1, 300);
$mc->set("produk:halaman:2", $produkHalaman2, 300);
// ✗ Anti-pattern 2: TTL terlalu panjang untuk data yang sering berubah
$mc->set('harga:produk:42', 15000000, 86400); // 24 jam — terlalu lama untuk harga!
// ✓ TTL yang sesuai dengan frekuensi perubahan data
$mc->set('harga:produk:42', 15000000, 300); // 5 menit — lebih masuk akal
// ✗ Anti-pattern 3: kunci tanpa TTL untuk data yang harusnya expired
$mc->set('session:user:42', $sessionData, 0); // tidak expired!
// Jika Memcached restart, data hilang; jika tidak restart, menumpuk terus
// ✓ Selalu set TTL yang masuk akal
$mc->set('session:user:42', $sessionData, 1800); // 30 menit
// ✗ Anti-pattern 4: tidak handle cache miss dengan baik (cache stampede)
// Banyak request bersamaan miss cache dan semua hit database!
// Gunakan dogpile lock seperti contoh di atas
// ✗ Anti-pattern 5: gunakan Memcached untuk data yang butuh persistensi
// Memcached tidak punya persistensi — saat server restart, semua data hilang
// Jangan simpan data penting (seperti session yang kritis) HANYA di Memcached
Ringkasan #
- Ekstensi
Memcached(bukanMemcache) adalah pilihan modern — lebih kaya fitur, menggunakan libmemcached, mendukung CAS, consistent hashing, dan server pool.- Distributed caching adalah kekuatan utama Memcached — tambah
addServers()dengan bobot, dan Memcached mendistribusikan kunci secara otomatis menggunakan consistent hashing.- CAS (Check and Set) untuk atomic update tanpa race condition — baca nilai dengan token, update hanya jika token masih valid. Jika
RES_DATA_EXISTS, retry loop.- Memcached tidak bisa wildcard delete — gunakan namespace versioning: simpan versi namespace di kunci terpisah dan increment untuk invalidasi seluruh grup.
- TTL dengan jitter (
3600 + random_int(-300, 300)) mencegah cache stampede — semua kunci tidak expired bersamaan dalam satu detik.- Dogpile lock menggunakan
add()(atomic: hanya berhasil jika kunci belum ada) untuk memastikan hanya satu request yang hit database saat cache miss bersamaan.- Persistent connection (
new Memcached('pool-id')) — koneksi dipertahankan antar request PHP di FPM, mengurangi overhead TCP handshake.- Pilih Redis jika memulai proyek baru — lebih serbaguna, fitur lebih lengkap, dan tidak banyak kehilangan performa untuk use case caching biasa.