On this page
Distributed Cache
Distributed caches provide shared, low-latency data storage across multiple application instances. Redis is the most popular choice in Java ecosystems.
Cache Patterns
Cache-Aside (Lazy Loading)
Application manages cache explicitly:
public Product getProduct(String sku) {
String key = "product:" + sku;
Product cached = (Product) redisTemplate.opsForValue().get(key);
if (cached != null) return cached;
Product product = productRepository.findBySku(sku).orElseThrow();
redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
return product;
}
Read-Through
Cache loads data automatically on miss (via LoadingCache or Redis modules).
Write-Through
Write to cache and database synchronously:
@Transactional
public Product updateProduct(Product product) {
Product saved = productRepository.save(product);
redisTemplate.opsForValue().set("product:" + saved.getSku(), saved, Duration.ofMinutes(30));
return saved;
}
Write-Behind (Write-Back)
Write to cache immediately, flush to database asynchronously. Higher performance but risk of data loss.
Cache Invalidation
The hardest problem in computer science:
// Event-driven invalidation
@CacheEvict(value = "products", key = "#product.sku")
public Product updateProduct(Product product) { return productRepository.save(product); }
// Pub/Sub for multi-instance invalidation
@Service
public class CacheInvalidationService {
@EventListener
public void onProductUpdated(ProductUpdatedEvent event) {
redisTemplate.convertAndSend("cache:invalidate",
"product:" + event.getSku());
}
@RedisMessageListenerContainer
public void handleInvalidation(String key) {
localCache.invalidate(key);
}
}
Multi-Level Cache (L1 + L2)
Request → L1 (Caffeine, local) → L2 (Redis, shared) → Database
~100ns ~1ms ~10ms
@Service
public class MultiLevelCacheService {
private final LoadingCache<String, Product> localCache;
private final RedisTemplate<String, Product> redisTemplate;
private final ProductRepository repository;
public Product getProduct(String sku) {
return localCache.get(sku, key -> {
Product cached = (Product) redisTemplate.opsForValue().get("product:" + key);
if (cached != null) return cached;
Product product = repository.findBySku(key).orElseThrow();
redisTemplate.opsForValue().set("product:" + key, product, Duration.ofMinutes(30));
return product;
});
}
}
Cache Stampede Prevention
When cache expires, many requests hit the database simultaneously:
// Solution 1: Lock-based
public Product getProduct(String sku) {
Product cached = getFromCache(sku);
if (cached != null) return cached;
String lockKey = "lock:product:" + sku;
if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(10))) {
try {
Product product = repository.findBySku(sku).orElseThrow();
setCache(sku, product);
return product;
} finally {
redisTemplate.delete(lockKey);
}
}
Thread.sleep(50);
return getProduct(sku); // retry
}
// Solution 2: Random TTL jitter
Duration ttl = Duration.ofMinutes(30).plusSeconds(ThreadLocalRandom.current().nextInt(300));
Redis Cluster for HA
spring:
data:
redis:
cluster:
nodes:
- redis-1:6379
- redis-2:6379
- redis-3:6379
max-redirects: 3
Best Practices
- Set TTL on all cache entries — no eternal caches
- Use cache-aside for most scenarios; write-through when consistency is critical
- Implement multi-level caching for high-QPS read paths
- Prevent cache stampede with locking or jitter
- Monitor hit rate, memory usage, and eviction rate