Memcached

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 #

AspekMemcachedRedis
Tipe dataHanya string/serializedString, 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 overheadLebih rendahSedikit lebih tinggi
Use case terbaikCache murni, high throughputCache + 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 (bukan Memcache) 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.

← Sebelumnya: Redis   Berikutnya: Artikel & Sumber Daya →

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