Advanced Redis Caching Strategies: Cache-Aside, Write-Through, and Stampede Prevention
Shubham Prakash 2024-12-18 7 min read min read
RedisCachingSpring BootSystem DesignPerformance
## The Cost of Database Read Contention
As application traffic grows, database read contention rapidly becomes the primary bottleneck for system scalability. Integrating an in-memory datastore like Redis reduces read latency from milliseconds to microseconds.
However, poorly implemented caching patterns frequently introduce stale data anomalies, memory bloat, and devastating cache stampedes.
## Core Caching Topologies
### 1. Cache-Aside (Lazy Loading)
The application code directly coordinates between the cache and the primary database: - Read request arrives -> Check Redis. - If hit -> Return immediately. - If miss -> Query PostgreSQL, write result to Redis with TTL, and return.
As application traffic grows, database read contention rapidly becomes the primary bottleneck for system scalability. Integrating an in-memory datastore like Redis reduces read latency from milliseconds to microseconds.
However, poorly implemented caching patterns frequently introduce stale data anomalies, memory bloat, and devastating cache stampedes.
## Core Caching Topologies
### 1. Cache-Aside (Lazy Loading)
The application code directly coordinates between the cache and the primary database: - Read request arrives -> Check Redis. - If hit -> Return immediately. - If miss -> Query PostgreSQL, write result to Redis with TTL, and return.
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id", unless = "#result == null")
public ProductDto getProductById(String id) {
return productRepository.findById(id)
.map(this::mapToDto)
.orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id));
}
@CacheEvict(value = "products", key = "#id")
public void updateProduct(String id, ProductUpdateRequest request) {
// Mutates database and invalidates the cached entry
productRepository.update(id, request);
}
}## Preventing Common Cache Failure Modes
### Cache Penetration Occurs when queries for non-existent keys repeatedly bypass the cache, pounding the database. - **Solution**: Cache null values with short TTLs (e.g. 60 seconds), or employ a Bloom filter at the API Gateway.
### Cache Stampede (Dog-piling) Occurs when a high-traffic key expires, causing hundreds of concurrent threads to simultaneously query the database to regenerate the cache. - **Solution**: Distributed locking using Redisson or probabilistic early expiration (XFetch algorithm).
RLock lock = redissonClient.getLock("lock:product:" + id);
if (lock.tryLock(100, 5000, TimeUnit.MILLISECONDS)) {
try {
// Double-check cache inside lock
ProductDto cached = redisTemplate.opsForValue().get(key);
if (cached != null) return cached;
ProductDto fresh = fetchFromDatabase(id);
redisTemplate.opsForValue().set(key, fresh, Duration.ofMinutes(15));
return fresh;
} finally {
lock.unlock();
}
}## Key Takeaways
1. Always set an explicit TTL on every cache key to prevent unbounded memory growth. 2. Separate transient session/rate-limit caches from persistent business data caches. 3. Use JSON serializers (such as Jackson2JsonRedisSerializer) over default Java binary serialization for maintainability and debugging.