Memcached #
Before Redis dominated the in-memory cache world, Memcached was the top choice powering the scale of Facebook, Twitter, and Wikipedia. Its philosophy is very simple and hasn’t changed since its release in 2003: one thing, done very well — storing key-value pairs in memory as fast as possible. No complex data structures, no persistence, no pub/sub, no scripting. Just caching. This simplicity isn’t a shortcoming — it’s a design choice that produces a very lightweight implementation, memory efficient and easy to scale horizontally. In the Java context, there are two mature and widely used client libraries: Spymemcached from Couchbase and XMemcached, which offers a fuller feature set. Understanding Memcached isn’t just about learning another tool — it teaches you the caching foundations that underpin all modern caching systems.
Memcached Architecture #
Memcached has several architectural characteristics that are important to understand before using it.
The Slab Allocator #
One of Memcached’s most fundamental differences from an ordinary cache is how it manages memory. Instead of dynamically allocating memory per item (which causes fragmentation), Memcached uses a slab allocator — memory is divided into slab classes based on size, and each item is stored in the matching slab class.
Total Memory: 512 MB
Slab Class 1 (size 96 bytes) ████████████████ → stores small values
Slab Class 2 (size 120 bytes) ████████████ → stores medium values
Slab Class 3 (size 152 bytes) ████████ → stores larger values
...
Slab Class N (size 1 MB) ██ → stores large values
The consequence: if you store a 100-byte value in a slab class sized at 120 bytes, 20 bytes are wasted (internal fragmentation). This is a deliberate tradeoff for constant-speed allocation — Memcached never needs to malloc when storing a new item.
Single-Threaded per Core vs Multi-Threaded #
Memcached uses a multi-threaded model with worker threads. Each TCP connection is handled by one worker thread, and operations within one connection are serial. This differs from Redis, which is single-threaded (until Redis 6.0) but uses an event loop.
Horizontal Scaling — Shared-Nothing #
Unlike Redis, which supports clustering with replication, Memcached has no built-in clustering. Scaling happens on the client side — the client decides which server a key goes to, usually using consistent hashing. This makes a Memcached cluster a “shared-nothing” system that’s very easy to scale: just add a server, and clients automatically redistribute keys.
flowchart LR
APP["Java Application\nSpymemcached / XMemcached"]
APP -->|hash(key) % 3 = 0| M1["Memcached\nServer 1\n192.168.1.1:11211"]
APP -->|hash(key) % 3 = 1| M2["Memcached\nServer 2\n192.168.1.2:11211"]
APP -->|hash(key) % 3 = 2| M3["Memcached\nServer 3\n192.168.1.3:11211"]When a Memcached server is added to or removed from a cluster, consistent hashing minimizes the number of keys that need to move — only about 1/N of keys are affected (N = number of servers). Without consistent hashing (plain modulo), almost all keys move, causing a massive cache stampede.Setting Up Dependencies #
Spymemcached #
Spymemcached is Couchbase’s asynchronous client — lightweight and easy to use:
<!-- 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 offers a fuller feature set, including per-server connection pools, the binary protocol, and namespace support:
<!-- 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'
Run Memcached locally with Docker:
# Default Memcached — 64 MB memory, port 11211
docker run -d \
--name memcached \
-p 11211:11211 \
memcached:1.6-alpine
# Memcached with more memory
docker run -d \
--name memcached \
-p 11211:11211 \
memcached:1.6-alpine \
memcached -m 512 -c 1024 -t 4
# -m 512 → allocate 512 MB of memory
# -c 1024 → max 1024 simultaneous connections
# -t 4 → 4 worker threads
# Access via telnet for debugging
telnet localhost 11211
Connecting with Spymemcached #
Spymemcached uses an NIO-based asynchronous model — a single thread handles all I/O for all connections to the 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 {
// Connect to a single server
public static MemcachedClient createSingle() throws IOException {
return new MemcachedClient(
new java.net.InetSocketAddress("localhost", 11211)
);
}
// Connect to multiple servers (cluster)
// The client automatically uses consistent hashing for key distribution
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"
)
);
}
// Connect with advanced configuration
public static MemcachedClient createWithConfig() throws IOException {
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
// Protocol: BINARY is more efficient than TEXT (default)
builder.setProtocol(ConnectionFactoryBuilder.Protocol.BINARY);
// Failure mode — what to do if a server can't be reached
// FailureMode.Redistribute → send to another server (default, safe for caches)
// FailureMode.Retry → keep trying the same server
// FailureMode.Cancel → cancel the operation, throw an exception
builder.setFailureMode(FailureMode.Redistribute);
// Operation timeout
builder.setOpTimeout(1000); // 1 second
// Connections per server
builder.setMaxReconnectDelay(30);
return new MemcachedClient(
builder.build(),
AddrUtil.getAddresses("localhost:11211")
);
}
}
Connecting with XMemcached #
XMemcached provides a more intuitive synchronous API with per-server connection pool support:
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 {
// Single server connection
public static MemcachedClient createSingle() throws Exception {
return new XMemcachedClientBuilder("localhost:11211").build();
}
// Cluster connection with full configuration
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 is the consistent hashing algorithm widely used by memcached
builder.setSessionLocator(new KetamaMemcachedSessionLocator());
// Binary protocol — more efficient than the text protocol
builder.setCommandFactory(new BinaryCommandFactory());
// Connection pool — number of TCP connections per server
// More than 1 allows parallel requests to the same server
builder.setConnectionPoolSize(2);
// Timeouts in milliseconds
builder.setConnectTimeout(3000);
builder.setOpTimeout(1000);
// Health check — XMemcached automatically reconnects if the connection drops
MemcachedClient client = builder.build();
client.setEnableHeartBeat(true);
return client;
}
}
Basic Operations #
Memcached only supports one data type: string (binary-safe). All more complex data structures must be serialized by the client before storing.
Set, Get, Delete #
public class MemcachedBasicOps {
// ===== SPYMEMCACHED =====
public void demoSpy(MemcachedClient client) throws Exception {
// SET — store a value with a TTL (in seconds)
// TTL 0 → never expires (until eviction or server restart)
// Max TTL → 30 days (2592000 seconds). Beyond that it's treated as a Unix timestamp
client.set("key", 300, "string-value");
// GET — fetch the value (synchronous — blocks until a response)
Object value = client.get("key");
System.out.println("Value: " + value);
// GET with a cast
String str = (String) client.get("key");
// DELETE
client.delete("key");
// ADD — set only if the key does NOT exist (doesn't overwrite)
client.add("new-key", 300, "value");
// REPLACE — set only if the key ALREADY exists (doesn't create)
client.replace("new-key", 600, "new-value");
// APPEND / PREPEND — append a string to the end of a value (requires the binary protocol)
// Only for string values, no-op if the key doesn't exist
client.append(0, "new-key", "-suffix");
client.prepend(0, "new-key", "prefix-");
// INCR / DECR — atomic operations on numeric values
// The initial value must be a numeric string: "0", "100", etc.
client.set("counter", 0, "0");
client.incr("counter", 1); // "1"
client.incr("counter", 10); // "11"
client.decr("counter", 3); // "8"
// Async GET — non-blocking, returns a Future
net.spy.memcached.internal.GetFuture<Object> future = client.asyncGet("new-key");
Object asyncResult = future.get(1000, java.util.concurrent.TimeUnit.MILLISECONDS);
}
// ===== XMEMCACHED =====
public void demoX(MemcachedClient client) throws Exception {
// The XMemcached API is more straightforward — all synchronous with timeouts
client.set("key", 300, "value");
String value = client.get("key");
System.out.println("Value: " + value);
client.delete("key");
boolean added = client.add("new-key", 300, "value");
System.out.println("Added successfully (key didn't exist): " + added);
boolean replaced = client.replace("new-key", 600, "updated-value");
System.out.println("Replaced successfully (key existed): " + replaced);
// INCR / DECR with an initial value if the key doesn't exist
long result = client.incr("counter", 1, 0); // delta=1, defaultValue=0
System.out.println("Counter: " + result);
}
}
Java Object Serialization #
Memcached stores byte arrays. Spymemcached and XMemcached automatically serialize Java objects using built-in Java Serialization. But Java Serialization is slow and produces large output — in production, use a more efficient serialization.
The Problem with Default Java Serialization #
// ✗ ANTI-PATTERN: storing Java objects with default serialization
// Problems: slow, large output, not portable across different JVM versions
public class UserProfile implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String email;
private int age;
// getters/setters...
}
// This WORKS but isn't efficient
client.set("user:1001", 300, new UserProfile("Budi", "[email protected]", 30));
UserProfile user = (UserProfile) client.get("user:1001");
Manual Serialization with 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();
}
// Store an object as a JSON string
public <T> void set(String key, int ttlSeconds, T value) throws Exception {
String json = objectMapper.writeValueAsString(value);
client.set(key, ttlSeconds, json);
}
// Fetch and deserialize an object from a 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);
}
// Usage example
public static void example(MemcachedClient client) throws Exception {
JsonMemcachedCache cache = new JsonMemcachedCache(client);
// Store
UserProfile user = new UserProfile("Sari", "[email protected]", 25);
cache.set("user:1002", 300, user);
// Fetch
UserProfile cached = cache.get("user:1002", UserProfile.class);
System.out.println("Name: " + cached.getName());
}
}
// The object doesn't need to be Serializable with JSON
public class UserProfile {
private String name;
private String email;
private int age;
public UserProfile() {} // Jackson needs a no-arg constructor
public UserProfile(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
public String getName() { return name; }
public String getEmail() { return email; }
public int getAge() { return age; }
public void setName(String name) { this.name = name; }
public void setEmail(String email) { this.email = email; }
public void setAge(int age) { this.age = age; }
}
Custom Transcoder in Spymemcached #
Spymemcached supports Transcoder — an interface for controlling how objects are serialized before being sent to 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("Failed to serialize: " + e.getMessage(), e);
}
}
@Override
public T decode(CachedData d) {
try {
return objectMapper.readValue(d.getData(), type);
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize: " + e.getMessage(), e);
}
}
@Override
public int getMaxSize() {
return CachedData.MAX_SIZE;
}
}
// How to use a custom transcoder
public class TranscoderDemo {
public static void demo(MemcachedClient client) throws Exception {
JacksonTranscoder<UserProfile> transcoder = new JacksonTranscoder<>(UserProfile.class);
// Set with a transcoder
client.set("user:1003", 300, new UserProfile("Dani", "[email protected]", 32), transcoder);
// Get with a transcoder — get the right type directly without casting
UserProfile user = client.get("user:1003", transcoder);
System.out.println("Name: " + user.getName());
}
}
Multi-Get — Batch Read Efficiency #
One of Memcached’s advantages for read-heavy workloads is its very efficient multi-get support. With one request, you can retrieve hundreds of values at once.
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class MemcachedMultiGet {
// ✗ ANTI-PATTERN: fetching one by one in a loop
// Every get() = 1 round trip to the 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 trips for N keys
}
// ===== SPYMEMCACHED =====
// ✓ CORRECT: getBulk — fetch everything at once in 1-2 round trips
public void bulkGetSpy(MemcachedClient client) throws Exception {
// Prepare the keys
List<String> keys = Arrays.asList(
"user:1001", "user:1002", "user:1003",
"user:1004", "user:1005"
);
// getBulk sends requests to all relevant servers at once
// and waits for all responses before returning
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());
}
// Keys not in the cache don't appear in the results
System.out.printf("Requested %d, found %d%n", keys.size(), results.size());
// Async bulk get — non-blocking
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("product:A", "product:B", "product:C");
// getByKeys — multi-get with 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 with Multi-Set #
public class CacheWarmer {
private final net.rubyeye.xmemcached.MemcachedClient client;
public CacheWarmer(net.rubyeye.xmemcached.MemcachedClient client) {
this.client = client;
}
// Pre-populate the cache with frequently accessed data
public void warmUpProductCache(List<Product> products) throws Exception {
for (Product product : products) {
String key = "cache:product:" + product.id();
String json = new com.fasterxml.jackson.databind.ObjectMapper()
.writeValueAsString(product);
client.set(key, 3600, json); // cache for 1 hour
}
System.out.println("Cache warmed up for " + products.size() + " products");
}
record Product(String id, String name, double price) {}
}
CAS — Check-And-Set for Concurrency #
CAS (Check-And-Set, or Compare-And-Swap) is Memcached’s mechanism for avoiding race conditions when multiple clients update the same key simultaneously. Every item in Memcached has a CAS token — a number that changes every time the item is updated.
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 matches, value updated, new cas=43)
B->>M: cas("counter", cas=42, "11")
M-->>B: EXISTS ✗ (cas doesn't match — A already changed the value)
Note over B: B needs to gets() again and retryimport net.spy.memcached.CASValue;
import net.spy.memcached.CASResponse;
public class MemcachedCASDemo {
// ✗ ANTI-PATTERN: GET then SET without CAS — race condition!
public void updateWithoutCAS(MemcachedClient client, String key) throws Exception {
String value = (String) client.get(key); // Clients A and B both GET "10"
int counter = Integer.parseInt(value) + 1;
client.set(key, 300, String.valueOf(counter)); // Both SET "11" — one update lost!
}
// ✓ CORRECT: GETS then CAS — atomic update
public boolean incrementWithCAS(MemcachedClient client,
String key, int maxRetries) throws Exception {
for (int attempt = 0; attempt < maxRetries; attempt++) {
// GETS — like GET but also returns the CAS token
CASValue<Object> casValue = client.gets(key);
if (casValue == null) {
// Key doesn't exist — initialize with ADD (atomic)
client.add(key, 300, "0");
continue;
}
String oldValue = (String) casValue.getValue();
long casToken = casValue.getCas();
String newValue = String.valueOf(Integer.parseInt(oldValue) + 1);
// CAS — set only if the CAS token is still the same
CASResponse response = client.cas(key, casToken, 300, newValue);
switch (response) {
case OK:
System.out.println("Update successful: " + oldValue + " → " + newValue);
return true;
case EXISTS:
// The value was already changed by another client — try again
System.out.println("CAS conflict, retrying... (attempt " + (attempt + 1) + ")");
Thread.sleep(10 + (long)(Math.random() * 20)); // small jitter
break;
case NOT_FOUND:
// The key was deleted during the operation
System.out.println("Key not found during CAS");
return false;
}
}
System.err.println("Failed to update after " + maxRetries + " attempts");
return false;
}
// ===== XMEMCACHED — CAS is cleaner with 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 oldValue = getsResponse.getValue();
long cas = getsResponse.getCas();
String newValue = String.valueOf(Integer.parseInt(oldValue) + 1);
boolean success = client.cas(key, 300, newValue, cas);
System.out.println("CAS " + (success ? "succeeded" : "failed — value changed"));
}
}
Caching Patterns #
Cache-Aside with Memcached #
public class MemcachedCacheAside {
private final net.rubyeye.xmemcached.MemcachedClient cache;
private final ArticleRepository repository;
private final com.fasterxml.jackson.databind.ObjectMapper mapper;
public MemcachedCacheAside(net.rubyeye.xmemcached.MemcachedClient cache,
ArticleRepository repository) {
this.cache = cache;
this.repository = repository;
this.mapper = new com.fasterxml.jackson.databind.ObjectMapper();
}
public Article getArticle(long articleId) throws Exception {
String cacheKey = "article:" + articleId;
// 1. Try the cache
String cached = cache.get(cacheKey);
if (cached != null) {
System.out.println("Cache HIT: " + cacheKey);
return mapper.readValue(cached, Article.class);
}
// 2. Cache MISS — fetch from the database
System.out.println("Cache MISS: " + cacheKey);
Article article = repository.findById(articleId);
if (article != null) {
// 3. Store in the cache
cache.set(cacheKey, 600, mapper.writeValueAsString(article)); // 10 minutes
}
return article;
}
public void updateArticle(long articleId, Article updated) throws Exception {
// Update the database
repository.update(articleId, updated);
// Invalidate the cache
cache.delete("article:" + articleId);
System.out.println("Cache invalidated: article:" + articleId);
}
// Multi-get for an article list (list/index pages)
public List<Article> getArticleBatch(List<Long> ids) throws Exception {
List<String> keys = ids.stream()
.map(id -> "article:" + id)
.toList();
// Fetch everything in the cache at once
Map<String, String> cached = cache.get(keys);
List<Article> results = new java.util.ArrayList<>();
List<Long> missedIds = new java.util.ArrayList<>();
for (Long id : ids) {
String key = "article:" + id;
if (cached.containsKey(key)) {
results.add(mapper.readValue(cached.get(key), Article.class));
} else {
missedIds.add(id);
}
}
// Fetch the misses from the database
if (!missedIds.isEmpty()) {
List<Article> fromDb = repository.findByIds(missedIds);
for (Article article : fromDb) {
// Store in the cache
cache.set("article:" + article.id(), 600,
mapper.writeValueAsString(article));
results.add(article);
}
}
System.out.printf("Batch: %d from cache, %d from DB%n",
ids.size() - missedIds.size(), missedIds.size());
return results;
}
interface ArticleRepository {
Article findById(long id);
void update(long id, Article article);
List<Article> findByIds(List<Long> ids);
}
record Article(long id, String title, String content) {}
}
Namespaces and Mass Key Invalidation #
Memcached doesn’t support prefix or pattern-based deletion (unlike Redis’s SCAN + DEL). A common trick for mass invalidation is using a namespace key that stores a version:
public class MemcachedNamespace {
private final net.rubyeye.xmemcached.MemcachedClient client;
public MemcachedNamespace(net.rubyeye.xmemcached.MemcachedClient client) {
this.client = client;
}
// Get the current namespace version
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;
}
// Build a key that includes the namespace version
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));
}
// Invalidate all keys in a namespace — just increment the version
// All old keys become automatically "invisible" because their keys have old versions
// Old keys expire naturally per their own TTLs
public void invalidateNamespace(String namespace) throws Exception {
client.incr(namespace + ":version", 1, 1L);
System.out.println("Namespace '" + namespace + "' invalidated.");
}
// Usage example
public static void example(net.rubyeye.xmemcached.MemcachedClient client) throws Exception {
MemcachedNamespace ns = new MemcachedNamespace(client);
ns.set("products", "123", 3600, "{\"name\":\"Laptop\"}");
ns.set("products", "456", 3600, "{\"name\":\"Mouse\"}");
String laptop = ns.get("products", "123");
System.out.println("Laptop: " + laptop);
// Invalidate all product caches at once
ns.invalidateNamespace("products");
// Now get("products", "123") returns null (the old-version key isn't found)
String laptopAfter = ns.get("products", "123");
System.out.println("Laptop after invalidation: " + laptopAfter); // null
}
}
Memcached vs Redis — Which to Choose #
This is the most frequently asked question when choosing a caching layer. Both are good for caching, but have different sweet spots.
| Aspect | Memcached | Redis |
|---|---|---|
| Data structures | Only string/binary | String, Hash, List, Set, Sorted Set, etc. |
| Memory efficiency | More efficient for simple values | Larger per-key overhead |
| Persistence | None — data lost on restart | RDB snapshots + AOF log |
| Replication | Not built-in | Built-in primary-replica |
| Clustering | Client-side (consistent hashing) | Built-in Redis Cluster |
| Multi-threading | Yes — several worker threads | Single-threaded (I/O), multi-threaded (Redis 6+) |
| Atomic operations | INCR/DECR, CAS | Much richer — Lua scripts, transactions |
| Pub/Sub | Not available | Yes |
| Max value size | 1 MB | 512 MB |
| List/set operations | Not available | LRANGE, ZADD, SINTER, etc. |
CHOOSE MEMCACHED WHEN:
✓ You only need simple caching (string key-value)
✓ Memory efficiency is critical — small values, large volume
✓ You need very easy horizontal scale-out (add nodes without coordination)
✓ Multi-threading matters for using all CPU cores
✓ You don't need persistence — an empty cache on restart is fine
✓ You already have a running Memcached infrastructure
CHOOSE REDIS WHEN:
✓ You need data structures beyond simple strings
✓ You need persistence (data must survive restarts)
✓ You need distributed locks, pub/sub, or Lua scripting
✓ You need built-in replication and high availability
✓ You need leaderboards (Sorted Sets), sessions (Hashes), or queues (Lists)
✓ The team isn't familiar with either — Redis has a richer ecosystem
flowchart TD
A{"Need data structures\nbeyond strings?"} -- Yes --> REDIS[Redis]
A -- No --> B{"Need persistence\nor replication?"}
B -- Yes --> REDIS
B -- No --> C{"Is memory efficiency\ncritical?"}
C -- Yes --> D{"Does multi-threading\nmatter?"}
C -- No --> REDIS
D -- Yes --> MEMCACHED[Memcached]
D -- No --> REDISSummary #
- Memcached is a pure cache — no persistence, no complex data structures, no built-in clustering. This simplicity is its strength: very fast and memory efficient.
- The slab allocator eliminates memory fragmentation at the cost of a little space (internal fragmentation). This results in constant, predictable memory allocation.
- Horizontal scaling happens on the client side using consistent hashing (Ketama). Use
KetamaMemcachedSessionLocatorin XMemcached or make sureKetamaConnectionFactoryis used in Spymemcached for a stable cluster.- Avoid default Java Serialization — slow and wasteful. Use JSON (Jackson) or more efficient binary serializers (Protobuf, Kryo) to store objects.
- Multi-get (
getBulk) is the performance key for read-heavy workloads — one request for hundreds of keys is far more efficient than a one-by-one GET loop.- CAS (Check-And-Set) prevents race conditions when multiple clients update the same key. Always use
gets()+cas()for updates that need atomicity, notget()+set().- Namespace versioning is the technique for mass invalidation without per-key DELETEs — just increment the namespace version, and all old keys become automatically invisible.
- Choose Memcached for pure caching with high memory efficiency and simple horizontal scaling. Choose Redis if you need rich data structures, persistence, or extra features like Pub/Sub and distributed locks.