Memcached #

Memcached adalah sistem caching terdistribusi in-memory yang didesain dengan prinsip kesederhanaan ekstrem — satu tipe data (string/bytes), satu operasi utama (get/set/delete), tanpa persistensi, tanpa replikasi bawaan. Justru karena kesederhanaannya itulah Memcached sangat cepat dan mudah di-scale secara horizontal: tambah server baru, distribusikan key menggunakan consistent hashing, selesai. Memcached adalah pilihan tepat ketika kamu butuh pure caching yang ringan tanpa overhead fitur tambahan. Memahami keterbatasannya — batas ukuran key/value, tidak ada persistensi, tidak ada struktur data kompleks — adalah kunci untuk memilih kapan Memcached adalah alat yang tepat.

Instalasi #

pip install pymemcache

Untuk menjalankan Memcached secara lokal:

# Docker
docker run -d --name memcached -p 11211:11211 memcached:latest

# Dengan batas memori 256MB
docker run -d --name memcached -p 11211:11211 memcached:latest memcached -m 256 -t 4

Membuat Koneksi #

pymemcache menyediakan beberapa jenis client: Client untuk single server, PooledClient untuk multi-threading, dan HashClient untuk distribusi ke beberapa server.

import json
import os
from pymemcache.client.base import Client, PooledClient
from pymemcache.client.hash import HashClient

MEMCACHED_HOST = os.getenv("MEMCACHED_HOST", "localhost")
MEMCACHED_PORT = int(os.getenv("MEMCACHED_PORT", "11211"))

# ANTI-PATTERN: Client biasa di lingkungan multi-thread
client = Client(("localhost", 11211))  # ✗ -- tidak thread-safe tanpa pool

# BENAR: PooledClient untuk aplikasi web (thread-safe dengan connection pool)
client = PooledClient(
    (MEMCACHED_HOST, MEMCACHED_PORT),
    max_pool_size=10,      # jumlah koneksi dalam pool
    pool_idle_timeout=60,  # tutup koneksi idle setelah 60 detik
    connect_timeout=5,     # timeout saat koneksi
    timeout=3,             # timeout saat operasi
    serde=None             # kita gunakan custom serde di bawah
)

# Tes koneksi
try:
    client.set("ping", "pong", expire=5)
    assert client.get("ping") == b"pong"
    print("Koneksi Memcached berhasil.")
except Exception as e:
    print(f"Koneksi gagal: {e}")

JSON Serializer #

Secara default, pymemcache menyimpan dan mengembalikan bytes. Untuk bekerja dengan dict dan list, implementasikan custom serializer.

import json
from pymemcache.client.base import PooledClient

class JSONSerde:
    """Serializer/deserializer JSON untuk pymemcache."""

    def serialize(self, key, value):
        if isinstance(value, (dict, list)):
            return json.dumps(value, ensure_ascii=False).encode("utf-8"), 2
        if isinstance(value, str):
            return value.encode("utf-8"), 1
        return value, 0

    def deserialize(self, key, value, flags):
        if flags == 2:
            return json.loads(value.decode("utf-8"))
        if flags == 1:
            return value.decode("utf-8")
        return value

client = PooledClient(
    (MEMCACHED_HOST, MEMCACHED_PORT),
    max_pool_size=10,
    connect_timeout=5,
    timeout=3,
    serde=JSONSerde()    # aktifkan JSON serializer
)

# Sekarang bisa simpan dan ambil dict secara transparan
client.set("pengguna:42", {"id": 42, "nama": "Budi", "email": "[email protected]"})
pengguna = client.get("pengguna:42")
print(type(pengguna))    # <class 'dict'>
print(pengguna["nama"])  # Budi

Operasi Dasar #

# Set dengan TTL (expire dalam detik)
client.set("kunci", "nilai", expire=3600)       # expire 1 jam
client.set("token:abc", "user-42", expire=300)  # expire 5 menit
client.set("permanent", "data")                 # tanpa TTL (sampai evicted)

# Get -- kembalikan None jika tidak ada atau expired
nilai = client.get("kunci")
print(nilai)   # "nilai" (jika pakai JSONSerde)

# Delete
client.delete("kunci")

# add() -- set hanya jika key belum ada (atomik, seperti SET NX di Redis)
berhasil = client.add("lock:proses", "1", expire=30)
print("Berhasil set:", berhasil)   # True jika baru, False jika sudah ada

# replace() -- set hanya jika key sudah ada
berhasil = client.replace("kunci", "nilai_baru", expire=3600)

Pola Cache-Aside #

from typing import Callable, Any

def get_atau_set(client, cache_key: str, fetch_fn: Callable, ttl_detik: int = 3600) -> Any:
    """
    Cache-Aside: cek cache → hit? kembalikan; miss? fetch, simpan, kembalikan.
    """
    # Sanitasi key -- Memcached tidak izinkan spasi, maks 250 karakter
    cache_key = cache_key.replace(" ", "_")[:250]

    nilai = client.get(cache_key)
    if nilai is not None:
        print(f"Cache HIT: {cache_key}")
        return nilai

    print(f"Cache MISS: {cache_key}")
    data = fetch_fn()

    if data is not None:
        client.set(cache_key, data, expire=ttl_detik)

    return data

def ambil_produk_dari_db(produk_id: int) -> dict:
    return {"id": produk_id, "nama": "Laptop Gaming", "harga": 18500000}

produk = get_atau_set(
    client,
    cache_key=f"produk:{101}",
    fetch_fn=lambda: ambil_produk_dari_db(101),
    ttl_detik=1800
)

Operasi Batch #

# Set banyak key sekaligus
client.set_many({
    "produk:101": {"id": 101, "nama": "Laptop Gaming",     "harga": 18500000},
    "produk:102": {"id": 102, "nama": "Samsung Galaxy S24", "harga": 15000000},
    "produk:103": {"id": 103, "nama": "Nike Air Max",       "harga": 1800000},
}, expire=1800)

# Get banyak key sekaligus -- lebih efisien dari loop get()
keys  = ["produk:101", "produk:102", "produk:103", "produk:999"]
hasil = client.get_many(keys)
# hasil adalah dict {key: value} -- hanya key yang ditemukan

for key in keys:
    if key in hasil:
        print(f"HIT: {key}{hasil[key]['nama']}")
    else:
        print(f"MISS: {key}")

# Delete banyak key sekaligus
client.delete_many(["produk:101", "produk:102", "produk:103"])

Increment dan Decrement Atomik #

# Counter halaman dilihat
client.set("views:artikel:5", "0", expire=86400)  # nilai harus string/bytes untuk incr

client.incr("views:artikel:5", 1)    # +1 (atomik)
client.incr("views:artikel:5", 10)   # +10
nilai = client.get("views:artikel:5")
print(f"Views: {nilai}")  # b'11'

# Rate limiting sederhana
def cek_rate_limit(client, user_id: str, maks: int = 100, window: int = 60) -> bool:
    key = f"rl:{user_id}"
    try:
        jumlah = client.incr(key, 1)
        if jumlah is None:
            # Key belum ada -- set dengan TTL
            client.set(key, "1", expire=window)
            return True
        return int(jumlah) <= maks
    except Exception:
        return True   # fail open jika Memcached error

for i in range(5):
    diizinkan = cek_rate_limit(client, "user-42", maks=3, window=60)
    print(f"Request {i+1}: {'✓ diizinkan' if diizinkan else '✗ ditolak'}")

HashClient — Distribusi Multi-Server #

HashClient mendistribusikan key ke beberapa server Memcached menggunakan consistent hashing — cara standar scaling horizontal Memcached.

from pymemcache.client.hash import HashClient

# Daftar server dari environment variable
servers_env = os.getenv("MEMCACHED_SERVERS", "localhost:11211")
servers_raw = [s.split(":") for s in servers_env.split(",")]
servers     = [(host, int(port)) for host, port in servers_raw]

cluster = HashClient(
    servers,
    serde=JSONSerde(),
    connect_timeout=5,
    timeout=3,
    use_pooling=True,     # connection pooling per server
    max_pool_size=5,
    retry_attempts=2,     # coba server lain jika tidak merespons
    retry_timeout=0.1,
    dead_timeout=30       # tandai server mati selama 30 detik
)

# Penggunaan identik dengan PooledClient
cluster.set("produk:200", {"id": 200, "nama": "MacBook Pro"}, expire=3600)
print(cluster.get("produk:200"))
Saat menambah atau menghapus server dari cluster Memcached, sebagian key akan di-rehash ke server berbeda dan mengakibatkan cache miss massal. Tambahkan server baru secara bertahap dan lakukan cache warming sebelum server lama dihapus untuk memitigasi dampaknya.

Namespace Versioning untuk Bulk Invalidation #

Memcached tidak mendukung pattern matching seperti KEYS produk:* di Redis. Untuk menginvalidasi sekelompok key sekaligus, gunakan version counter per namespace.

def get_namespace_version(client, namespace: str) -> int:
    versi = client.get(f"ns:{namespace}")
    if versi is None:
        client.set(f"ns:{namespace}", "1", expire=86400)
        return 1
    return int(versi)

def build_key(client, namespace: str, key: str) -> str:
    versi = get_namespace_version(client, namespace)
    return f"{namespace}:v{versi}:{key}"

def invalidasi_namespace(client, namespace: str) -> None:
    """Invalidasi seluruh group -- increment version, semua key lama tidak valid."""
    client.incr(f"ns:{namespace}", 1)
    print(f"Namespace '{namespace}' diinvalidasi.")

# Simpan dengan namespace
key = build_key(client, "produk", "101")
client.set(key, {"id": 101, "nama": "Laptop"}, expire=3600)

# Ambil dengan namespace
nilai = client.get(build_key(client, "produk", "101"))
print(nilai)

# Invalidasi semua cache produk sekaligus (tanpa delete satu per satu)
invalidasi_namespace(client, "produk")
# Semua key "produk:v1:..." tidak akan pernah ditemukan lagi
# Key baru otomatis menggunakan "produk:v2:..."

Penanganan Error dan Fallback #

from pymemcache.exceptions import MemcacheError

def set_cache_aman(client, key: str, nilai: Any, expire: int = 3600) -> bool:
    try:
        # Validasi ukuran -- Memcached maks 1MB per value
        serialized = json.dumps(nilai, ensure_ascii=False)
        if len(serialized.encode("utf-8")) > 900_000:
            print(f"Value terlalu besar untuk di-cache ({len(serialized)} bytes)")
            return False

        client.set(key, nilai, expire=expire)
        return True
    except MemcacheError as e:
        print(f"Memcached error: {e}")
        return False

def ambil_data_dengan_fallback(client, key: str, fetch_fn: Callable) -> Any:
    """Ambil dari cache, fallback ke source jika cache down."""
    try:
        cached = client.get(key)
        if cached is not None:
            return cached
    except Exception:
        pass   # Memcached tidak tersedia, langsung ke source

    data = fetch_fn()
    try:
        if data is not None:
            client.set(key, data, expire=3600)
    except Exception:
        pass   # Tidak bisa simpan cache, tapi data tetap dikembalikan

    return data

Memcached vs Redis #

Pilih Memcached jika:
  ✓ Butuh pure caching sederhana dan ringan
  ✓ Ingin scale horizontal dengan banyak server (multi-threaded, efisien per core)
  ✓ Data yang di-cache hanya berupa string/bytes sederhana
  ✓ Footprint memory lebih kecil menjadi prioritas

Pilih Redis jika:
  ✓ Butuh struktur data: Hash, List, Set, Sorted Set, Stream
  ✓ Butuh persistensi data (RDB/AOF snapshot)
  ✓ Butuh Pub/Sub, Distributed Lock, atau Lua scripting
  ✓ Butuh atomic operations yang kompleks (pipeline + WATCH)
  ✓ Ingin satu alat untuk caching + messaging + queue

Keterbatasan Memcached yang perlu diingat:
  ✗ Key maks 250 karakter, tidak boleh ada spasi
  ✗ Value maks 1MB per key
  ✗ Tidak ada persistensi -- data hilang saat restart
  ✗ Tidak ada replikasi bawaan
  ✗ Tidak ada pattern matching untuk bulk invalidation

Ringkasan #

  • PooledClient bukan Client — gunakan selalu di lingkungan multi-thread; Client biasa tidak thread-safe tanpa pooling.
  • Custom JSONSerde — implementasikan untuk bisa menyimpan dan mengambil dict/list tanpa serialize/deserialize manual setiap saat.
  • add() untuk atomic set-if-not-exists — gunakan untuk distributed lock sederhana atau mencegah overwrite; lebih aman dari set() + get() yang non-atomic.
  • get_many() dan set_many() — selalu gunakan operasi batch untuk multiple key; satu network round-trip untuk banyak key.
  • HashClient untuk multi-server — distribusikan key ke beberapa server dengan consistent hashing; konfigurasi retry_attempts dan dead_timeout untuk toleransi kegagalan.
  • Namespace versioning — karena tidak ada pattern matching, gunakan version counter per namespace untuk menginvalidasi sekelompok key sekaligus secara efisien.
  • Validasi ukuran sebelum simpan — Memcached menolak value lebih dari 1MB; cek ukuran sebelum set() untuk menghindari error runtime.
  • Sanitasi key — key tidak boleh mengandung spasi atau karakter kontrol; replace spasi dan batasi ke 250 karakter.
  • Fail open untuk error — tangkap semua exception dan lanjut ke database; cache adalah optimasi, bukan titik kegagalan tunggal.
  • Pilih Redis untuk lebih dari sekadar caching — jika butuh struktur data, persistensi, atau Pub/Sub, Redis adalah pilihan yang tepat; Memcached unggul hanya untuk pure key-value caching sederhana.

← Sebelumnya: Redis   Berikutnya: Django →

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