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 #
PooledClientbukanClient— gunakan selalu di lingkungan multi-thread;Clientbiasa 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 dariset()+get()yang non-atomic.get_many()danset_many()— selalu gunakan operasi batch untuk multiple key; satu network round-trip untuk banyak key.HashClientuntuk multi-server — distribusikan key ke beberapa server dengan consistent hashing; konfigurasiretry_attemptsdandead_timeoutuntuk 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.