Memcached #
Sebelum Redis mendominasi dunia in-memory cache, Memcached adalah pilihan utama yang menggerakkan skala Facebook, Twitter, dan Wikipedia. Filosofinya sangat sederhana dan tidak pernah berubah sejak dirilis tahun 2003: satu hal, dilakukan dengan sangat baik — menyimpan pasangan key-value di memori secepat mungkin. Tidak ada struktur data kompleks, tidak ada persistence, tidak ada pub/sub, tidak ada scripting. Hanya cache. Kesederhanaan ini bukan kekurangan — ia adalah pilihan desain yang menghasilkan implementasi yang sangat ringan, efisien secara memori, dan mudah di-scale secara horizontal. Dalam konteks Java, ada dua client library utama yang matang dan banyak digunakan: Spymemcached dari Couchbase dan XMemcached yang menawarkan fitur lebih lengkap. Memahami Memcached bukan hanya soal mengenal tool lain — ia mengajarkan kamu tentang fondasi cache yang menjadi basis semua sistem caching modern.
Arsitektur Memcached #
Memcached punya beberapa karakteristik arsitektur yang penting untuk dipahami sebelum menggunakannya.
Slab Allocator #
Salah satu perbedaan paling mendasar Memcached dari cache biasa adalah cara ia mengelola memori. Alih-alih mengalokasikan memori secara dinamis per item (yang menyebabkan fragmentasi), Memcached menggunakan slab allocator — memori dibagi ke dalam slab class berdasarkan ukuran, dan setiap item disimpan dalam slab class yang sesuai.
Memori Total: 512 MB
Slab Class 1 (ukuran 96 bytes) ████████████████ → simpan nilai kecil
Slab Class 2 (ukuran 120 bytes) ████████████ → simpan nilai sedang
Slab Class 3 (ukuran 152 bytes) ████████ → simpan nilai lebih besar
...
Slab Class N (ukuran 1 MB) ██ → simpan nilai besar
Akibatnya: jika kamu menyimpan nilai 100 bytes dalam slab class yang berukuran 120 bytes, ada 20 bytes yang terbuang (internal fragmentation). Ini adalah tradeoff yang disengaja demi kecepatan alokasi yang konstan — Memcached tidak pernah perlu melakukan malloc saat menyimpan item baru.
Single-Threaded per Core vs Multi-Threaded #
Memcached menggunakan model multi-threaded dengan worker threads. Setiap koneksi TCP ditangani oleh satu worker thread, dan operasi dalam satu koneksi bersifat serial. Ini berbeda dari Redis yang single-threaded (sampai Redis 6.0) tapi menggunakan event loop.
Skala Horizontal — Shared-Nothing #
Berbeda dari Redis yang mendukung clustering dengan replikasi, Memcached tidak punya built-in clustering. Scaling dilakukan di sisi client — client yang memutuskan ke server mana sebuah kunci dikirim, biasanya menggunakan consistent hashing. Ini menjadikan Memcached cluster sebagai sistem “shared-nothing” yang sangat mudah di-scale: cukup tambah server, client otomatis mendistribusikan ulang kunci.
flowchart LR
APP[Aplikasi Java\nSpymemcached / XMemcached]
APP -->|hash(kunci) % 3 = 0| M1[Memcached\nServer 1\n192.168.1.1:11211]
APP -->|hash(kunci) % 3 = 1| M2[Memcached\nServer 2\n192.168.1.2:11211]
APP -->|hash(kunci) % 3 = 2| M3[Memcached\nServer 3\n192.168.1.3:11211]Ketika sebuah server Memcached ditambahkan atau dihapus dari cluster, consistent hashing meminimalkan jumlah kunci yang perlu dipindahkan — hanya sekitar 1/N kunci yang terdampak (N = jumlah server baru). Tanpa consistent hashing (plain modulo), hampir semua kunci berpindah dan menyebabkan cache stampede besar-besaran.Setup Dependencies #
Spymemcached #
Spymemcached adalah client asynchronous dari Couchbase, ringan dan mudah dipakai:
<!-- Maven -->
<dependency>
<groupId>net.spy</groupId>
<artifactId>spymemcached</artifactId>
<version>2.12.3</version>
</dependency>
// Gradle
implementation 'net.spy:spymemcached:2.12.3'
XMemcached #
XMemcached menawarkan fitur lebih lengkap termasuk connection pool per server, binary protocol, dan dukungan namespace:
<!-- Maven -->
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>2.4.8</version>
</dependency>
// Gradle
implementation 'com.googlecode.xmemcached:xmemcached:2.4.8'
Jalankan Memcached secara lokal dengan Docker:
# Memcached default — 64 MB memori, port 11211
docker run -d \
--name memcached \
-p 11211:11211 \
memcached:1.6-alpine
# Memcached dengan memori lebih besar
docker run -d \
--name memcached \
-p 11211:11211 \
memcached:1.6-alpine \
memcached -m 512 -c 1024 -t 4
# -m 512 → alokasi memori 512 MB
# -c 1024 → maks 1024 koneksi simultan
# -t 4 → 4 worker threads
# Akses via telnet untuk debugging
telnet localhost 11211
Koneksi dengan Spymemcached #
Spymemcached menggunakan model asynchronous berbasis NIO — satu thread menangani semua I/O untuk semua koneksi ke cluster.
import net.spy.memcached.AddrUtil;
import net.spy.memcached.MemcachedClient;
import net.spy.memcached.DefaultConnectionFactory;
import net.spy.memcached.ConnectionFactoryBuilder;
import net.spy.memcached.FailureMode;
import java.io.IOException;
public class SpymemcachedFactory {
// Koneksi ke satu server
public static MemcachedClient createSingle() throws IOException {
return new MemcachedClient(
new java.net.InetSocketAddress("localhost", 11211)
);
}
// Koneksi ke beberapa server (cluster)
// Client otomatis menggunakan consistent hashing untuk distribusi kunci
public static MemcachedClient createCluster() throws IOException {
return new MemcachedClient(
AddrUtil.getAddresses(
"192.168.1.1:11211 192.168.1.2:11211 192.168.1.3:11211"
)
);
}
// Koneksi dengan konfigurasi lanjutan
public static MemcachedClient createWithConfig() throws IOException {
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
// Protocol: BINARY lebih efisien dari TEXT (default)
builder.setProtocol(ConnectionFactoryBuilder.Protocol.BINARY);
// Failure mode — apa yang dilakukan jika server tidak bisa dihubungi
// FailureMode.Redistribute → kirim ke server lain (default, aman untuk cache)
// FailureMode.Retry → terus coba server yang sama
// FailureMode.Cancel → batalkan operasi, lempar exception
builder.setFailureMode(FailureMode.Redistribute);
// Timeout operasi
builder.setOpTimeout(1000); // 1 detik
// Jumlah koneksi per server
builder.setMaxReconnectDelay(30);
return new MemcachedClient(
builder.build(),
AddrUtil.getAddresses("localhost:11211")
);
}
}
Koneksi dengan XMemcached #
XMemcached menyediakan API synchronous yang lebih intuitif dengan dukungan connection pool per server:
import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.XMemcachedClientBuilder;
import net.rubyeye.xmemcached.algorithm.KetamaMemcachedSessionLocator;
import net.rubyeye.xmemcached.command.BinaryCommandFactory;
import net.rubyeye.xmemcached.utils.AddrUtil;
public class XMemcachedFactory {
// Koneksi single server
public static MemcachedClient createSingle() throws Exception {
return new XMemcachedClientBuilder("localhost:11211").build();
}
// Koneksi cluster dengan konfigurasi lengkap
public static MemcachedClient createCluster() throws Exception {
XMemcachedClientBuilder builder = new XMemcachedClientBuilder(
AddrUtil.getAddresses("192.168.1.1:11211 192.168.1.2:11211 192.168.1.3:11211")
);
// Consistent hashing (KetamaMemcachedSessionLocator)
// Ketama adalah algoritma consistent hashing yang dipakai oleh memcached secara luas
builder.setSessionLocator(new KetamaMemcachedSessionLocator());
// Binary protocol — lebih efisien dari text protocol
builder.setCommandFactory(new BinaryCommandFactory());
// Connection pool — jumlah koneksi TCP per server
// Lebih dari 1 memungkinkan request paralel ke server yang sama
builder.setConnectionPoolSize(2);
// Timeout dalam milidetik
builder.setConnectTimeout(3000);
builder.setOpTimeout(1000);
// Health check — XMemcached secara otomatis reconnect jika koneksi terputus
MemcachedClient client = builder.build();
client.setEnableHeartBeat(true);
return client;
}
}
Operasi Dasar #
Memcached hanya mendukung satu tipe data: string (binary-safe). Semua struktur data yang lebih kompleks harus diserialisasi oleh client sebelum disimpan.
Set, Get, Delete #
public class MemcachedBasicOps {
// ===== SPYMEMCACHED =====
public void demoSpy(MemcachedClient client) throws Exception {
// SET — simpan nilai dengan TTL (dalam detik)
// TTL 0 → tidak pernah expire (sampai eviction atau restart server)
// TTL maks → 30 hari (2592000 detik). Lebih dari itu dianggap Unix timestamp
client.set("kunci", 300, "nilai-string");
// GET — ambil nilai (synchronous — blokir sampai ada respons)
Object nilai = client.get("kunci");
System.out.println("Nilai: " + nilai);
// GET dengan cast
String str = (String) client.get("kunci");
// DELETE
client.delete("kunci");
// ADD — set hanya jika kunci BELUM ada (tidak menimpa)
client.add("kunci-baru", 300, "nilai");
// REPLACE — set hanya jika kunci SUDAH ada (tidak membuat baru)
client.replace("kunci-baru", 600, "nilai-baru");
// APPEND / PREPEND — tambahkan string ke ujung nilai (butuh binary protocol)
// Hanya untuk nilai string, tidak berlaku jika kunci tidak ada
client.append(0, "kunci-baru", "-tambahan");
client.prepend(0, "kunci-baru", "awalan-");
// INCR / DECR — operasi atomic pada nilai numerik
// Nilai awal harus berupa string angka: "0", "100", dll
client.set("counter", 0, "0");
client.incr("counter", 1); // "1"
client.incr("counter", 10); // "11"
client.decr("counter", 3); // "8"
// GET async — tidak blokir, kembalikan Future
net.spy.memcached.internal.GetFuture<Object> future = client.asyncGet("kunci-baru");
Object hasilAsync = future.get(1000, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// ===== XMEMCACHED =====
public void demoX(MemcachedClient client) throws Exception {
// API XMemcached lebih straightforward — semua synchronous dengan timeout
client.set("kunci", 300, "nilai");
String nilai = client.get("kunci");
System.out.println("Nilai: " + nilai);
client.delete("kunci");
boolean ditambah = client.add("kunci-baru", 300, "nilai");
System.out.println("Berhasil ditambah (kunci belum ada): " + ditambah);
boolean diganti = client.replace("kunci-baru", 600, "nilai-diperbarui");
System.out.println("Berhasil diganti (kunci sudah ada): " + diganti);
// INCR / DECR dengan nilai awal jika kunci belum ada
long hasil = client.incr("counter", 1, 0); // delta=1, defaultValue=0
System.out.println("Counter: " + hasil);
}
}
Serialisasi Objek Java #
Memcached menyimpan byte array. Spymemcached dan XMemcached otomatis melakukan serialisasi objek Java menggunakan Java Serialization bawaan. Tapi Java Serialization lambat dan menghasilkan output besar — di production, gunakan serialisasi yang lebih efisien.
Masalah dengan Java Serialization Default #
// ✗ ANTI-PATTERN: menyimpan objek Java dengan serialisasi default
// Masalah: slow, output besar, tidak portable antar versi JVM berbeda
public class UserProfile implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private String nama;
private String email;
private int umur;
// getter/setter...
}
// Ini BEKERJA tapi tidak efisien
client.set("user:1001", 300, new UserProfile("Budi", "[email protected]", 30));
UserProfile user = (UserProfile) client.get("user:1001");
Serialisasi Manual dengan JSON #
import com.fasterxml.jackson.databind.ObjectMapper;
import net.spy.memcached.MemcachedClient;
public class JsonMemcachedCache {
private final MemcachedClient client;
private final ObjectMapper objectMapper;
public JsonMemcachedCache(MemcachedClient client) {
this.client = client;
this.objectMapper = new ObjectMapper();
}
// Simpan objek sebagai JSON string
public <T> void set(String key, int ttlSeconds, T value) throws Exception {
String json = objectMapper.writeValueAsString(value);
client.set(key, ttlSeconds, json);
}
// Ambil dan deserialisasi objek dari JSON string
public <T> T get(String key, Class<T> type) throws Exception {
String json = (String) client.get(key);
if (json == null) return null;
return objectMapper.readValue(json, type);
}
// Contoh penggunaan
public static void contoh(MemcachedClient client) throws Exception {
JsonMemcachedCache cache = new JsonMemcachedCache(client);
// Simpan
UserProfile user = new UserProfile("Sari", "[email protected]", 25);
cache.set("user:1002", 300, user);
// Ambil
UserProfile cached = cache.get("user:1002", UserProfile.class);
System.out.println("Nama: " + cached.getNama());
}
}
// Objek tidak perlu Serializable jika pakai JSON
public class UserProfile {
private String nama;
private String email;
private int umur;
public UserProfile() {} // Jackson butuh no-arg constructor
public UserProfile(String nama, String email, int umur) {
this.nama = nama;
this.email = email;
this.umur = umur;
}
public String getNama() { return nama; }
public String getEmail() { return email; }
public int getUmur() { return umur; }
public void setNama(String nama) { this.nama = nama; }
public void setEmail(String email) { this.email = email; }
public void setUmur(int umur) { this.umur = umur; }
}
Transcoder Kustom di Spymemcached #
Spymemcached mendukung Transcoder — antarmuka untuk mengontrol bagaimana objek diserialisasi sebelum dikirim ke Memcached:
import net.spy.memcached.transcoders.Transcoder;
import net.spy.memcached.CachedData;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JacksonTranscoder<T> implements Transcoder<T> {
private static final int FLAGS = 0;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Class<T> type;
public JacksonTranscoder(Class<T> type) {
this.type = type;
}
@Override
public boolean asyncDecode(CachedData d) {
return false;
}
@Override
public CachedData encode(T o) {
try {
byte[] bytes = objectMapper.writeValueAsBytes(o);
return new CachedData(FLAGS, bytes, CachedData.MAX_SIZE);
} catch (Exception e) {
throw new RuntimeException("Gagal serialize: " + e.getMessage(), e);
}
}
@Override
public T decode(CachedData d) {
try {
return objectMapper.readValue(d.getData(), type);
} catch (Exception e) {
throw new RuntimeException("Gagal deserialize: " + e.getMessage(), e);
}
}
@Override
public int getMaxSize() {
return CachedData.MAX_SIZE;
}
}
// Cara pemakaian transcoder kustom
public class TranscoderDemo {
public static void demo(MemcachedClient client) throws Exception {
JacksonTranscoder<UserProfile> transcoder = new JacksonTranscoder<>(UserProfile.class);
// Set dengan transcoder
client.set("user:1003", 300, new UserProfile("Dani", "[email protected]", 32), transcoder);
// Get dengan transcoder — langsung dapat tipe yang tepat tanpa cast
UserProfile user = client.get("user:1003", transcoder);
System.out.println("Nama: " + user.getNama());
}
}
Multi-Get — Efisiensi Batch Read #
Salah satu keunggulan Memcached untuk read-heavy workload adalah dukungan multi-get yang sangat efisien. Dengan satu request, kamu bisa mendapatkan ratusan nilai sekaligus.
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class MemcachedMultiGet {
// ✗ ANTI-PATTERN: ambil satu per satu dalam loop
// Setiap get() = 1 round-trip ke server
public Map<String, String> getOneByOne(MemcachedClient client,
List<String> keys) throws Exception {
Map<String, String> results = new java.util.HashMap<>();
for (String key : keys) {
String value = (String) client.get(key);
if (value != null) results.put(key, value);
}
return results; // N round-trip untuk N kunci
}
// ===== SPYMEMCACHED =====
// ✓ BENAR: getBulk — ambil semua sekaligus dalam 1-2 round-trip
public void bulkGetSpy(MemcachedClient client) throws Exception {
// Siapkan kunci
List<String> keys = Arrays.asList(
"user:1001", "user:1002", "user:1003",
"user:1004", "user:1005"
);
// getBulk mengirim request ke semua server yang relevan sekaligus
// dan menunggu semua respons sebelum return
Map<String, Object> results = client.getBulk(keys);
for (Map.Entry<String, Object> entry : results.entrySet()) {
System.out.printf("Key: %s | Value: %s%n", entry.getKey(), entry.getValue());
}
// Kunci yang tidak ada di cache tidak muncul dalam hasil
System.out.printf("Diminta %d, ditemukan %d%n", keys.size(), results.size());
// Async bulk get — tidak blokir
net.spy.memcached.internal.BulkFuture<Map<String, Object>> future =
client.asyncGetBulk(keys);
Map<String, Object> asyncResults = future.get(2000, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// ===== XMEMCACHED =====
public void bulkGetX(net.rubyeye.xmemcached.MemcachedClient client) throws Exception {
List<String> keys = Arrays.asList("produk:A", "produk:B", "produk:C");
// getByKeys — multi-get dengan type inference
Map<String, String> results = client.get(keys);
results.forEach((k, v) -> System.out.printf("Key: %s | Value: %s%n", k, v));
}
}
Pattern: Cache Warming dengan Multi-Set #
public class CacheWarmer {
private final net.rubyeye.xmemcached.MemcachedClient client;
public CacheWarmer(net.rubyeye.xmemcached.MemcachedClient client) {
this.client = client;
}
// Pre-populate cache dengan data yang sering diakses
public void warmUpProdukCache(List<Produk> produks) throws Exception {
for (Produk produk : produks) {
String key = "cache:produk:" + produk.getId();
String json = new com.fasterxml.jackson.databind.ObjectMapper()
.writeValueAsString(produk);
client.set(key, 3600, json); // cache 1 jam
}
System.out.println("Cache warmed up untuk " + produks.size() + " produk");
}
record Produk(String id, String nama, double harga) {}
}
CAS — Check-And-Set untuk Concurrency #
CAS (Check-And-Set, atau Compare-And-Swap) adalah mekanisme Memcached untuk menghindari race condition saat beberapa client memperbarui kunci yang sama secara bersamaan. Setiap item di Memcached punya CAS token — angka yang berubah setiap kali item diperbarui.
sequenceDiagram
participant A as Client A
participant B as Client B
participant M as Memcached
A->>M: gets("counter")
M-->>A: value="10", cas=42
B->>M: gets("counter")
M-->>B: value="10", cas=42
A->>M: cas("counter", cas=42, "11")
M-->>A: STORED ✓ (cas cocok, nilai diperbarui, cas baru=43)
B->>M: cas("counter", cas=42, "11")
M-->>B: EXISTS ✗ (cas tidak cocok — A sudah mengubah nilai)
Note over B: B perlu gets() ulang dan coba lagiimport net.spy.memcached.CASValue;
import net.spy.memcached.CASResponse;
public class MemcachedCASDemo {
// ✗ ANTI-PATTERN: GET lalu SET tanpa CAS — race condition!
public void updateTanpaCAS(MemcachedClient client, String key) throws Exception {
String nilai = (String) client.get(key); // Client A dan B keduanya GET "10"
int counter = Integer.parseInt(nilai) + 1;
client.set(key, 300, String.valueOf(counter)); // Keduanya SET "11" — kehilangan satu update!
}
// ✓ BENAR: GETS lalu CAS — atomic update
public boolean incrementWithCAS(MemcachedClient client,
String key, int maxRetries) throws Exception {
for (int attempt = 0; attempt < maxRetries; attempt++) {
// GETS — seperti GET tapi juga mengembalikan CAS token
CASValue<Object> casValue = client.gets(key);
if (casValue == null) {
// Kunci tidak ada — inisialisasi dengan ADD (atomic)
client.add(key, 300, "0");
continue;
}
String nilaiLama = (String) casValue.getValue();
long casToken = casValue.getCas();
String nilaiBAru = String.valueOf(Integer.parseInt(nilaiLama) + 1);
// CAS — set hanya jika CAS token masih sama
CASResponse response = client.cas(key, casToken, 300, nilaiBAru);
switch (response) {
case OK:
System.out.println("Berhasil update: " + nilaiLama + " → " + nilaiBAru);
return true;
case EXISTS:
// Nilai sudah berubah oleh client lain — coba lagi
System.out.println("CAS conflict, coba lagi... (percobaan " + (attempt + 1) + ")");
Thread.sleep(10 + (long)(Math.random() * 20)); // jitter kecil
break;
case NOT_FOUND:
// Kunci sudah dihapus selama operasi
System.out.println("Kunci tidak ditemukan saat CAS");
return false;
}
}
System.err.println("Gagal update setelah " + maxRetries + " percobaan");
return false;
}
// ===== XMEMCACHED — CAS lebih rapi dengan GetsResponse =====
public void casWithXMemcached(net.rubyeye.xmemcached.MemcachedClient client,
String key) throws Exception {
net.rubyeye.xmemcached.GetsResponse<String> getsResponse = client.gets(key);
if (getsResponse == null) {
client.add(key, 300, "0");
return;
}
String nilaiLama = getsResponse.getValue();
long cas = getsResponse.getCas();
String nilaiBaru = String.valueOf(Integer.parseInt(nilaiLama) + 1);
boolean berhasil = client.cas(key, 300, nilaiBaru, cas);
System.out.println("CAS " + (berhasil ? "berhasil" : "gagal — nilai berubah"));
}
}
Pola Caching #
Cache-Aside dengan Memcached #
public class MemcachedCacheAside {
private final net.rubyeye.xmemcached.MemcachedClient cache;
private final ArtikelRepository repository;
private final com.fasterxml.jackson.databind.ObjectMapper mapper;
public MemcachedCacheAside(net.rubyeye.xmemcached.MemcachedClient cache,
ArtikelRepository repository) {
this.cache = cache;
this.repository = repository;
this.mapper = new com.fasterxml.jackson.databind.ObjectMapper();
}
public Artikel getArtikel(long artikelId) throws Exception {
String cacheKey = "artikel:" + artikelId;
// 1. Coba dari cache
String cached = cache.get(cacheKey);
if (cached != null) {
System.out.println("Cache HIT: " + cacheKey);
return mapper.readValue(cached, Artikel.class);
}
// 2. Cache MISS — ambil dari database
System.out.println("Cache MISS: " + cacheKey);
Artikel artikel = repository.findById(artikelId);
if (artikel != null) {
// 3. Simpan ke cache
cache.set(cacheKey, 600, mapper.writeValueAsString(artikel)); // 10 menit
}
return artikel;
}
public void updateArtikel(long artikelId, Artikel updated) throws Exception {
// Update database
repository.update(artikelId, updated);
// Invalidasi cache
cache.delete("artikel:" + artikelId);
System.out.println("Cache invalidated: artikel:" + artikelId);
}
// Multi-get untuk list artikel (halaman list/index)
public List<Artikel> getArtikelBatch(List<Long> ids) throws Exception {
List<String> keys = ids.stream()
.map(id -> "artikel:" + id)
.toList();
// Ambil semua yang ada di cache sekaligus
Map<String, String> cached = cache.get(keys);
List<Artikel> results = new java.util.ArrayList<>();
List<Long> missedIds = new java.util.ArrayList<>();
for (Long id : ids) {
String key = "artikel:" + id;
if (cached.containsKey(key)) {
results.add(mapper.readValue(cached.get(key), Artikel.class));
} else {
missedIds.add(id);
}
}
// Ambil yang miss dari database
if (!missedIds.isEmpty()) {
List<Artikel> fromDb = repository.findByIds(missedIds);
for (Artikel artikel : fromDb) {
// Simpan ke cache
cache.set("artikel:" + artikel.getId(), 600,
mapper.writeValueAsString(artikel));
results.add(artikel);
}
}
System.out.printf("Batch: %d dari cache, %d dari DB%n",
ids.size() - missedIds.size(), missedIds.size());
return results;
}
interface ArtikelRepository {
Artikel findById(long id);
void update(long id, Artikel artikel);
List<Artikel> findByIds(List<Long> ids);
}
record Artikel(long id, String judul, String konten) {}
}
Namespace dan Key Invalidasi Massal #
Memcached tidak mendukung penghapusan berdasarkan prefix atau pattern (tidak seperti Redis SCAN + DEL). Trick yang umum untuk invalidasi massal adalah menggunakan namespace key yang menyimpan versi:
public class MemcachedNamespace {
private final net.rubyeye.xmemcached.MemcachedClient client;
public MemcachedNamespace(net.rubyeye.xmemcached.MemcachedClient client) {
this.client = client;
}
// Ambil versi namespace saat ini
private long getNamespaceVersion(String namespace) throws Exception {
Long version = client.get(namespace + ":version");
if (version == null) {
client.set(namespace + ":version", 0, 1L);
return 1L;
}
return version;
}
// Buat kunci yang menyertakan versi namespace
private String buildKey(String namespace, String key) throws Exception {
long version = getNamespaceVersion(namespace);
return namespace + ":" + version + ":" + key;
}
public void set(String namespace, String key, int ttl, String value) throws Exception {
client.set(buildKey(namespace, key), ttl, value);
}
public String get(String namespace, String key) throws Exception {
return client.get(buildKey(namespace, key));
}
// Invalidasi semua kunci dalam namespace — cukup increment versi
// Semua kunci lama otomatis "tidak terlihat" karena kunci mereka punya versi lama
// Kunci lama akan expire secara natural sesuai TTL masing-masing
public void invalidateNamespace(String namespace) throws Exception {
client.incr(namespace + ":version", 1, 1L);
System.out.println("Namespace '" + namespace + "' di-invalidasi.");
}
// Contoh penggunaan
public static void contoh(net.rubyeye.xmemcached.MemcachedClient client) throws Exception {
MemcachedNamespace ns = new MemcachedNamespace(client);
ns.set("produk", "123", 3600, "{\"nama\":\"Laptop\"}");
ns.set("produk", "456", 3600, "{\"nama\":\"Mouse\"}");
String laptop = ns.get("produk", "123");
System.out.println("Laptop: " + laptop);
// Invalidasi semua cache produk sekaligus
ns.invalidateNamespace("produk");
// Sekarang get("produk", "123") akan return null (kunci versi lama tidak ketemu)
String laptopSetelah = ns.get("produk", "123");
System.out.println("Laptop setelah invalidasi: " + laptopSetelah); // null
}
}
Memcached vs Redis — Kapan Pilih yang Mana #
Ini adalah pertanyaan yang paling sering muncul ketika memilih caching layer. Keduanya baik untuk caching, tapi punya sweet spot yang berbeda.
| Aspek | Memcached | Redis |
|---|---|---|
| Struktur data | Hanya string/binary | String, Hash, List, Set, Sorted Set, dll |
| Efisiensi memori | Lebih hemat untuk nilai sederhana | Overhead per kunci lebih besar |
| Persistensi | Tidak ada — data hilang saat restart | RDB snapshot + AOF log |
| Replikasi | Tidak ada bawaan | Built-in primary-replica |
| Clustering | Sisi client (consistent hashing) | Built-in Redis Cluster |
| Multi-threading | Ya — beberapa worker thread | Single-threaded (I/O), multi-thread (Redis 6+) |
| Operasi atomik | INCR/DECR, CAS | Jauh lebih kaya — Lua script, transactions |
| Pub/Sub | Tidak ada | Ya |
| Ukuran nilai maks | 1 MB | 512 MB |
| Operasi list/set | Tidak ada | LRANGE, ZADD, SINTER, dll |
PILIH MEMCACHED JIKA:
✓ Hanya butuh cache sederhana (key-value string)
✓ Efisiensi memori sangat kritis — nilai kecil, volume besar
✓ Butuh scale-out horizontal yang sangat mudah (tambah node tanpa koordinasi)
✓ Multi-threading penting untuk pemanfaatan semua CPU core
✓ Tidak butuh persistence — cache boleh kosong saat restart
✓ Sudah punya infrastruktur Memcached yang berjalan
PILIH REDIS JIKA:
✓ Butuh struktur data lebih dari sekadar string
✓ Butuh persistence (data tidak boleh hilang saat restart)
✓ Butuh distributed lock, pub/sub, atau scripting Lua
✓ Butuh replikasi dan high availability built-in
✓ Butuh leaderboard (Sorted Set), session (Hash), atau queue (List)
✓ Tim belum familiar dengan keduanya — Redis punya ekosistem lebih kaya
flowchart TD
A{Butuh struktur\ndata selain string?} -- Ya --> REDIS[Redis]
A -- Tidak --> B{Butuh persistence\natau replikasi?}
B -- Ya --> REDIS
B -- Tidak --> C{Efisiensi memori\nsangat kritis?}
C -- Ya --> D{Multi-threading\npenting?}
C -- Tidak --> REDIS
D -- Ya --> MEMCACHED[Memcached]
D -- Tidak --> REDISRingkasan #
- Memcached adalah pure cache — tidak ada persistensi, tidak ada struktur data kompleks, tidak ada clustering built-in. Kesederhanaan ini adalah kekuatannya: sangat cepat dan hemat memori.
- Slab allocator menghilangkan fragmentasi memori dengan mengorbankan sedikit ruang (internal fragmentation). Ini menghasilkan alokasi memori yang konstan dan dapat diprediksi.
- Scaling horizontal dilakukan di sisi client menggunakan consistent hashing (Ketama). Gunakan
KetamaMemcachedSessionLocatordi XMemcached atau pastikanKetamaConnectionFactorydipakai di Spymemcached untuk cluster yang stabil.- Hindari Java Serialization default — lambat dan boros. Gunakan JSON (Jackson) atau serializer biner (Protobuf, Kryo) yang lebih efisien untuk menyimpan objek.
- Multi-get (
getBulk) adalah kunci performa untuk read-heavy workload — satu request untuk ratusan kunci jauh lebih efisien dari loop GET satu per satu.- CAS (Check-And-Set) mencegah race condition saat beberapa client memperbarui kunci yang sama. Selalu gunakan
gets()+cas()untuk update yang perlu atomisitas, bukanget()+set().- Namespace versioning adalah teknik untuk invalidasi massal tanpa DELETE per kunci — cukup increment versi namespace, semua kunci lama otomatis tidak terlihat.
- Pilih Memcached untuk cache murni dengan efisiensi memori tinggi dan skala horizontal sederhana. Pilih Redis jika kamu butuh struktur data kaya, persistensi, atau fitur tambahan seperti Pub/Sub dan distributed lock.