System Design Fundamentals: Implementing High-Throughput Redis Caching & Cache Stampede Prevention
1. Selecting the Correct Caching Pattern
In modern microservice architectures, caching is not merely about dropping an in-memory key-value store in front of a relational database. Selecting the incorrect caching pattern can introduce severe data inconsistency, stale read anomalies, and silent memory bloat across service clusters.
The most widely adopted strategy is the Cache-Aside (Lazy Loading) pattern. The application service queries Redis first; upon a cache miss, it fetches the source of truth from PostgreSQL or MySQL, populates the cache with a defined Time-To-Live (TTL), and returns the payload to the client.
For write-intensive transactional systems where low-latency writes are critical, the Write-Behind (Write-Back) pattern persists modifications to Redis immediately while asynchronously batching writes to the disk database via a background message consumer.
2. Mitigating Cache Stampede & Cache Penetration
A Cache Stampede (also known as the Thundering Herd problem) occurs when a high-traffic key holding hundreds of requests per second expires. All incoming threads experience a simultaneous cache miss and query the primary database together, causing connection pool exhaustion and database collapse.
To mitigate this in high-concurrency environments, backend systems implement distributed mutex locking using Redis SETNX (Set if Not Exists). Only the thread that successfully acquires the lock is permitted to query the database and recalculate the cache; other concurrent requests wait or consume slightly stale cached data temporarily.
// Acquiring a distributed lock to prevent thundering herd database calls
String lockKey = "lock:catalog:product:" + productId;
String requestId = UUID.randomUUID().toString();
// SET key value NX PX 30000 (Set if Not Exists, 30s expiry)
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, Duration.ofSeconds(30));
if (Boolean.TRUE.equals(acquired)) {
try {
ProductData data = productRepository.findById(productId);
redisTemplate.opsForValue().set("product:" + productId, data, Duration.ofMinutes(15));
} finally {
releaseDistributedLock(lockKey, requestId);
}
} else {
Thread.sleep(50);
return getCachedProduct(productId);
}
3. Choosing the Right Eviction Policy
When Redis reaches its allocated RAM limit, its configured eviction algorithm dictates which data gets purged to accommodate incoming write requests.
Using default 'noeviction' settings in production will cause write commands to fail with OutOfMemory errors once memory fills up. For general dynamic API caching, 'allkeys-lru' (Least Recently Used) or 'allkeys-lfu' (Least Frequently Used) provides optimal cache-hit ratios.