Introduction
Memcached is a high-performance, distributed memory object caching system that significantly improves application performance by reducing database load and decreasing latency. In Java applications, Memcached integration provides a powerful solution for caching frequently accessed data, session storage, and computation results.
Memcached Overview
What is Memcached?
- In-memory key-value store for small chunks of arbitrary data
- Distributed caching system with simple client-server architecture
- Volatile storage - data can be evicted when memory is full
- No persistence by default - purely in-memory cache
Key Features
- Simple protocol - easy to implement and use
- Distributed - can scale horizontally across multiple servers
- Fast - operations typically complete in microseconds
- Language agnostic - clients available for many programming languages
Java Client Libraries
1. SpyMemcached (Recommended)
<dependency> <groupId>net.spy</groupId> <artifactId>spymemcached</artifactId> <version>2.12.3</version> </dependency>
2. XMemcached
<dependency> <groupId>com.googlecode.xmemcached</groupId> <artifactId>xmemcached</artifactId> <version>2.4.7</version> </dependency>
3. AWS ElastiCache Client
<dependency> <groupId>net.spy</groupId> <artifactId>spymemcached</artifactId> <version>2.12.3</version> </dependency>
Basic Integration with SpyMemcached
1. Memcached Configuration Class
package com.example.cache.config;
import net.spy.memcached.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.ExecutionException;
@Configuration
public class MemcachedConfig {
@Value("${memcached.servers:localhost:11211}")
private String memcachedServers;
@Value("${memcached.operation.timeout:5000}")
private long operationTimeout;
@Bean
public MemcachedClient memcachedClient() throws IOException {
// Parse server addresses (format: "host1:port1,host2:port2")
String[] servers = memcachedServers.split(",");
InetSocketAddress[] addresses = new InetSocketAddress[servers.length];
for (int i = 0; i < servers.length; i++) {
String[] parts = servers[i].split(":");
String host = parts[0].trim();
int port = Integer.parseInt(parts[1].trim());
addresses[i] = new InetSocketAddress(host, port);
}
// Connection factory with configuration
ConnectionFactoryBuilder connectionFactoryBuilder = new ConnectionFactoryBuilder()
.setProtocol(ConnectionFactoryBuilder.Protocol.BINARY)
.setOpTimeout(operationTimeout)
.setTimeoutExceptionThreshold(998)
.setLocatorType(ConnectionFactoryBuilder.Locator.CONSISTENT)
.setFailureMode(FailureMode.Redistribute)
.setUseNagleAlgorithm(false)
.setReadBufferSize(16384);
return new MemcachedClient(connectionFactoryBuilder.build(), addresses);
}
}
2. Memcached Service Abstraction
package com.example.cache.service;
import net.spy.memcached.MemcachedClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@Service
public class MemcachedService {
private static final Logger logger = LoggerFactory.getLogger(MemcachedService.class);
private final MemcachedClient memcachedClient;
private final long defaultTimeout;
public MemcachedService(MemcachedClient memcachedClient) {
this.memcachedClient = memcachedClient;
this.defaultTimeout = 5; // seconds
}
// Basic operations
public boolean set(String key, int expiration, Object value) {
try {
Future<Boolean> future = memcachedClient.set(key, expiration, value);
return future.get(defaultTimeout, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("Failed to set key: {}", key, e);
return false;
}
}
public Object get(String key) {
try {
return memcachedClient.get(key);
} catch (Exception e) {
logger.error("Failed to get key: {}", key, e);
return null;
}
}
public boolean delete(String key) {
try {
Future<Boolean> future = memcachedClient.delete(key);
return future.get(defaultTimeout, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("Failed to delete key: {}", key, e);
return false;
}
}
// Advanced operations
public boolean add(String key, int expiration, Object value) {
try {
Future<Boolean> future = memcachedClient.add(key, expiration, value);
return future.get(defaultTimeout, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("Failed to add key: {}", key, e);
return false;
}
}
public boolean replace(String key, int expiration, Object value) {
try {
Future<Boolean> future = memcachedClient.replace(key, expiration, value);
return future.get(defaultTimeout, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("Failed to replace key: {}", key, e);
return false;
}
}
public long increment(String key, long by, long defaultValue) {
try {
return memcachedClient.incr(key, by, defaultValue);
} catch (Exception e) {
logger.error("Failed to increment key: {}", key, e);
return -1;
}
}
public long decrement(String key, long by, long defaultValue) {
try {
return memcachedClient.decr(key, by, defaultValue);
} catch (Exception e) {
logger.error("Failed to decrement key: {}", key, e);
return -1;
}
}
// Utility methods
public boolean exists(String key) {
return get(key) != null;
}
public void flush() {
try {
memcachedClient.flush();
} catch (Exception e) {
logger.error("Failed to flush cache", e);
}
}
}
Advanced Cache Manager Implementation
1. Generic Cache Manager
package com.example.cache.manager;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.spy.memcached.MemcachedClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class GenericCacheManager {
private static final Logger logger = LoggerFactory.getLogger(GenericCacheManager.class);
private final MemcachedClient memcachedClient;
private final ObjectMapper objectMapper;
public GenericCacheManager(MemcachedClient memcachedClient, ObjectMapper objectMapper) {
this.memcachedClient = memcachedClient;
this.objectMapper = objectMapper;
}
// JSON serialization/deserialization with type safety
public <T> boolean put(String key, T value, int expirationSeconds) {
try {
String jsonValue = objectMapper.writeValueAsString(value);
return memcachedClient.set(key, expirationSeconds, jsonValue).get(5, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("Failed to cache object with key: {}", key, e);
return false;
}
}
public <T> T get(String key, Class<T> clazz) {
try {
Object value = memcachedClient.get(key);
if (value instanceof String) {
return objectMapper.readValue((String) value, clazz);
}
return clazz.cast(value);
} catch (Exception e) {
logger.error("Failed to retrieve cached object with key: {}", key, e);
return null;
}
}
// Cache-aside pattern implementation
public <T> T getOrCompute(String key, Class<T> clazz, int expirationSeconds,
CacheLoader<T> loader) {
T value = get(key, clazz);
if (value == null) {
value = loader.load();
if (value != null) {
put(key, value, expirationSeconds);
}
}
return value;
}
// Bulk operations
public void putMany(java.util.Map<String, ?> keyValueMap, int expirationSeconds) {
keyValueMap.forEach((key, value) -> {
try {
String jsonValue = objectMapper.writeValueAsString(value);
memcachedClient.set(key, expirationSeconds, jsonValue);
} catch (Exception e) {
logger.error("Failed to cache object with key: {}", key, e);
}
});
}
@FunctionalInterface
public interface CacheLoader<T> {
T load();
}
}
2. Typed Cache Service
package com.example.cache.service;
import com.example.cache.manager.GenericCacheManager;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class TypedCacheService {
private final GenericCacheManager cacheManager;
// Cache namespaces
private static final String USER_CACHE_PREFIX = "user:";
private static final String PRODUCT_CACHE_PREFIX = "product:";
private static final String SESSION_CACHE_PREFIX = "session:";
// Expiration times in seconds
private static final int SHORT_TERM = 300; // 5 minutes
private static final int MEDIUM_TERM = 3600; // 1 hour
private static final int LONG_TERM = 86400; // 24 hours
public TypedCacheService(GenericCacheManager cacheManager) {
this.cacheManager = cacheManager;
}
// User cache methods
public void cacheUser(String userId, Object userData) {
String key = USER_CACHE_PREFIX + userId;
cacheManager.put(key, userData, MEDIUM_TERM);
}
public <T> T getUser(String userId, Class<T> clazz) {
String key = USER_CACHE_PREFIX + userId;
return cacheManager.get(key, clazz);
}
// Product cache methods
public void cacheProduct(String productId, Object productData) {
String key = PRODUCT_CACHE_PREFIX + productId;
cacheManager.put(key, productData, LONG_TERM);
}
public <T> T getProduct(String productId, Class<T> clazz) {
String key = PRODUCT_CACHE_PREFIX + productId;
return cacheManager.get(key, clazz);
}
// Session cache methods
public void cacheSession(String sessionId, Object sessionData) {
String key = SESSION_CACHE_PREFIX + sessionId;
cacheManager.put(key, sessionData, SHORT_TERM);
}
public <T> T getSession(String sessionId, Class<T> clazz) {
String key = SESSION_CACHE_PREFIX + sessionId;
return cacheManager.get(key, clazz);
}
public void invalidateSession(String sessionId) {
String key = SESSION_CACHE_PREFIX + sessionId;
// Delete immediately
// Note: You'd need access to memcachedClient or add delete method to GenericCacheManager
}
}
Spring Cache Abstraction with Memcached
1. Custom Cache Manager for Spring
package com.example.cache.spring;
import net.spy.memcached.MemcachedClient;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Configuration
@EnableCaching
public class MemcachedCacheConfig {
@Bean
public CacheManager cacheManager(MemcachedClient memcachedClient) {
MemcachedCacheManager cacheManager = new MemcachedCacheManager(memcachedClient);
cacheManager.setDefaultExpiration(3600); // 1 hour default
return cacheManager;
}
public static class MemcachedCacheManager implements CacheManager {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>();
private final MemcachedClient memcachedClient;
private int defaultExpiration = 0;
public MemcachedCacheManager(MemcachedClient memcachedClient) {
this.memcachedClient = memcachedClient;
}
public void setDefaultExpiration(int defaultExpiration) {
this.defaultExpiration = defaultExpiration;
}
@Override
public Cache getCache(String name) {
return cacheMap.computeIfAbsent(name,
key -> new MemcachedCache(key, memcachedClient, defaultExpiration));
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableCollection(cacheMap.keySet());
}
}
public static class MemcachedCache implements Cache {
private final String name;
private final MemcachedClient memcachedClient;
private final int defaultExpiration;
public MemcachedCache(String name, MemcachedClient memcachedClient, int defaultExpiration) {
this.name = name;
this.memcachedClient = memcachedClient;
this.defaultExpiration = defaultExpiration;
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return memcachedClient;
}
@Override
public ValueWrapper get(Object key) {
String cacheKey = getCacheKey(key);
Object value = memcachedClient.get(cacheKey);
return (value != null ? () -> value : null);
}
@Override
public <T> T get(Object key, Class<T> type) {
String cacheKey = getCacheKey(key);
Object value = memcachedClient.get(cacheKey);
if (value != null && type != null && !type.isInstance(value)) {
throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
}
return (T) value;
}
@Override
public void put(Object key, Object value) {
String cacheKey = getCacheKey(key);
memcachedClient.set(cacheKey, defaultExpiration, value);
}
@Override
public void evict(Object key) {
String cacheKey = getCacheKey(key);
memcachedClient.delete(cacheKey);
}
@Override
public void clear() {
// Note: This flushes entire cache - use with caution!
memcachedClient.flush();
}
private String getCacheKey(Object key) {
return name + ":" + key.toString();
}
}
}
2. Using Spring Cache Annotations
package com.example.service;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId")
public User getUserById(String userId) {
// This will only execute if user is not in cache
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
}
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
User updatedUser = userRepository.save(user);
return updatedUser;
}
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(String userId) {
userRepository.deleteById(userId);
}
@CacheEvict(value = "users", allEntries = true)
public void clearAllUserCache() {
// This will clear all entries in 'users' cache
}
}
Production-Ready Implementation
1. Health Check Integration
package com.example.cache.health;
import net.spy.memcached.MemcachedClient;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class MemcachedHealthIndicator implements HealthIndicator {
private final MemcachedClient memcachedClient;
public MemcachedHealthIndicator(MemcachedClient memcachedClient) {
this.memcachedClient = memcachedClient;
}
@Override
public Health health() {
try {
// Test connection with a simple operation
String testKey = "health-check";
String testValue = "ok";
memcachedClient.set(testKey, 10, testValue).get(2, TimeUnit.SECONDS);
Object retrieved = memcachedClient.get(testKey);
if (testValue.equals(retrieved)) {
return Health.up()
.withDetail("servers", memcachedClient.getAvailableServers())
.withDetail("unavailableServers", memcachedClient.getUnavailableServers())
.build();
} else {
return Health.down().withDetail("error", "Data integrity check failed").build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}
2. Monitoring and Metrics
package com.example.cache.metrics;
import io.micrometer.core.instrument.MeterRegistry;
import net.spy.memcached.MemcachedClient;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class MemcachedMetrics {
private final MemcachedClient memcachedClient;
private final MeterRegistry meterRegistry;
private final Map<String, AtomicLong> operationCounters = new ConcurrentHashMap<>();
public MemcachedMetrics(MemcachedClient memcachedClient, MeterRegistry meterRegistry) {
this.memcachedClient = memcachedClient;
this.meterRegistry = meterRegistry;
initializeMetrics();
}
private void initializeMetrics() {
// Initialize counters for different operations
String[] operations = {"get", "set", "delete", "hit", "miss"};
for (String op : operations) {
operationCounters.put(op, meterRegistry.gauge(
"memcached.operations." + op, new AtomicLong(0)));
}
}
public void recordOperation(String operation, boolean success) {
String key = operation + (success ? "" : ".error");
operationCounters.computeIfAbsent(key,
k -> meterRegistry.gauge("memcached.operations." + k, new AtomicLong(0)))
.incrementAndGet();
}
public void recordCacheHit() {
recordOperation("hit", true);
}
public void recordCacheMiss() {
recordOperation("miss", true);
}
@Scheduled(fixedRate = 60000) // Every minute
public void collectStats() {
// Collect and report cache statistics
meterRegistry.gauge("memcached.available_servers",
memcachedClient.getAvailableServers().size());
meterRegistry.gauge("memcached.unavailable_servers",
memcachedClient.getUnavailableServers().size());
}
}
3. Configuration Properties
# application.yml memcached: servers: "localhost:11211,localhost:11212" operation-timeout: 5000 pool: min-connections: 5 max-connections: 20 timeout: 3000 cache: expiration: user: 3600 # 1 hour product: 86400 # 24 hours session: 1800 # 30 minutes
Best Practices
1. Key Design
public class CacheKeyGenerator {
public static String generateUserKey(String userId) {
return String.format("user:%s", userId);
}
public static String generateProductKey(String productId) {
return String.format("product:%s", productId);
}
public static String generateSessionKey(String sessionId) {
return String.format("session:%s", sessionId);
}
// Use consistent key patterns for easy management
public static String generateCompositeKey(String type, String id, String version) {
return String.format("%s:%s:v%s", type, id, version);
}
}
2. Error Handling and Fallbacks
@Component
public class ResilientCacheService {
private final MemcachedService memcachedService;
private final Logger logger = LoggerFactory.getLogger(ResilientCacheService.class);
public <T> T getWithFallback(String key, Class<T> clazz, Supplier<T> fallback) {
try {
T value = memcachedService.get(key, clazz);
if (value != null) {
return value;
}
} catch (Exception e) {
logger.warn("Cache retrieval failed for key: {}, using fallback", key, e);
}
// Use fallback and optionally cache the result
T fallbackValue = fallback.get();
if (fallbackValue != null) {
try {
memcachedService.set(key, 300, fallbackValue); // Cache fallback result
} catch (Exception cacheException) {
logger.warn("Failed to cache fallback value for key: {}", key, cacheException);
}
}
return fallbackValue;
}
}
Testing
1. Integration Test
@SpringBootTest
@TestPropertySource(properties = "memcached.servers=localhost:11211")
class MemcachedIntegrationTest {
@Autowired
private MemcachedService memcachedService;
@Test
void testBasicOperations() {
String key = "test-key";
String value = "test-value";
// Test set and get
assertTrue(memcachedService.set(key, 60, value));
assertEquals(value, memcachedService.get(key));
// Test delete
assertTrue(memcachedService.delete(key));
assertNull(memcachedService.get(key));
}
}
2. Mock Testing
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private MemcachedClient memcachedClient;
@InjectMocks
private UserService userService;
@Test
void shouldReturnCachedUser() throws Exception {
String userId = "user-123";
User expectedUser = new User(userId, "John Doe");
when(memcachedClient.get("user:" + userId))
.thenReturn(expectedUser);
User result = userService.getUserById(userId);
assertEquals(expectedUser, result);
verify(memcachedClient, never()).set(anyString(), anyInt(), any());
}
}
Conclusion
Memcached integration in Java provides a robust solution for improving application performance through distributed caching. Key takeaways:
- Choose the right client library based on your needs (SpyMemcached for simplicity, XMemcached for advanced features)
- Implement proper abstraction to isolate caching logic
- Use connection pooling and proper configuration for production
- Implement health checks and monitoring
- Design cache keys strategically for easy management
- Handle failures gracefully with fallback mechanisms
- Integrate with Spring Cache for declarative caching
When implemented correctly, Memcached can significantly reduce database load, decrease response times, and improve overall application scalability.