
Redis Cache Strategy: Preventing Avalanche, Penetration, and Breakdown
Using Redis as a cache is one of the most common ways to speed up web applications and reduce database load. However, under high-concurrency workloads, improper cache design can lead to database failure.
Let us explore the three classic caching issues and how to prevent them in production.
1. Cache Avalanche
The Problem: Cache Avalanche occurs when a large number of cached keys expire at the exact same time, or when the Redis server goes down. Suddenly, all requests hit the database simultaneously, causing it to crash.
Solutions:
- Randomize TTL (Expiration Time): Add a random offset (e.g., 1-5 minutes) to the expiration time of each key so they do not expire together.
- High Availability: Deploy Redis with clustering, master-slave setup, or Sentinel to prevent the caching layer from failing entirely.
// Adding random variance to TTL
const baseTTL = 3600; // 1 hour
const randomOffset = Math.floor(Math.random() * 300); // 0-5 minutes
const finalTTL = baseTTL + randomOffset;2. Cache Penetration
The Problem: Cache Penetration happens when requests query data that does not exist in either the cache or the database (e.g., querying for a user ID of -1). Since the data never gets cached, every single query falls through directly to the database.
Solutions:
- Cache Null Objects: If the database returns no result, cache a null or placeholder value with a short expiration time (e.g., 30 seconds).
- Bloom Filters: Place a Bloom Filter in front of the cache. It is a space-efficient data structure that can tell you with 100% certainty if a key does not exist.
// Caching null values example
if (!dbResult) {
await redis.set(key, "NULL_VALUE", "EX", 30);
}3. Cache Breakdown
The Problem: Cache Breakdown occurs when a hot key (a key under heavy, constant traffic, like a trending article) expires. At that split second, thousands of concurrent requests query the key, miss the cache, and hit the database at the same time.
Solutions:
- Mutex Locks (Single Flight): Use a lock so that only the first request is allowed to query the database and update the cache. Other requests wait until the cache is populated.
- Never Expire Logically: Keep the key in Redis forever. Use a background worker to periodically refresh the data before it becomes stale.
// Pseudocode for using a mutex lock to fetch database
async function getHotKey(key) {
let value = await redis.get(key);
if (!value) {
if (await acquireLock(key + "_lock")) {
value = await fetchFromDatabase(key);
await redis.set(key, value, "EX", 3600);
await releaseLock(key + "_lock");
} else {
// Wait and retry
await sleep(100);
return getHotKey(key);
}
}
return value;
}Conclusion
Caching is powerful, but it requires careful design under load. By randomizing TTLs, caching null results, and using mutexes for hot keys, you can ensure your databases remain stable even during massive traffic spikes.