Memcached

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 lagi
import 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.

AspekMemcachedRedis
Struktur dataHanya string/binaryString, Hash, List, Set, Sorted Set, dll
Efisiensi memoriLebih hemat untuk nilai sederhanaOverhead per kunci lebih besar
PersistensiTidak ada — data hilang saat restartRDB snapshot + AOF log
ReplikasiTidak ada bawaanBuilt-in primary-replica
ClusteringSisi client (consistent hashing)Built-in Redis Cluster
Multi-threadingYa — beberapa worker threadSingle-threaded (I/O), multi-thread (Redis 6+)
Operasi atomikINCR/DECR, CASJauh lebih kaya — Lua script, transactions
Pub/SubTidak adaYa
Ukuran nilai maks1 MB512 MB
Operasi list/setTidak adaLRANGE, 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 --> REDIS

Ringkasan #

  • 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 KetamaMemcachedSessionLocator di XMemcached atau pastikan KetamaConnectionFactory dipakai 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, bukan get() + 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.

← Sebelumnya: Redis   Berikutnya: Spring Boot →

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