sz

Sejan

Blog

Building Resilient Redis Caching Architectures: Eviction Policies, Cluster Sharding, and Cache-Aside Patterns

Home › System Architecture › Building Resilient Redis Caching Architectures: Eviction Policies, Cluster Sharding, and Cache-Aside Patterns
Share
Building Resilient Redis Caching Architectures: Eviction Policies, Cluster Sharding, and Cache-Aside Patterns

In high-traffic web applications, the database is almost always the first component to hit throughput saturation. An in-memory key-value data store like Redis can deliver sub-millisecond read response times, handling hundreds of thousands of operations per second on a single instance.

However, treating Redis as a simple "key-value dump" without architectural planning leads to critical production hazards: cache stampedes (thundering herd), memory fragmentation, uncoordinated cache invalidation, and severe latency spikes during master failovers. In this deep dive, we explore the architectural blueprints required to build resilient, enterprise-scale Redis caching tiers.


1. Caching Access Patterns: Cache-Aside vs. Write-Through

Choosing the correct caching topology dictates your data consistency and database load profile:

Pattern Workflow Strengths & Trade-offs
Cache-Aside (Lazy Loading) Application checks Redis first. On cache miss, queries DB, populates Redis, and returns data. Resilient to cache node restarts; only requested data is cached. Initial request on cache miss suffers slight latency.
Write-Through Application writes to Redis and DB simultaneously within a transaction. Guarantees data freshness; reads never miss. Write latency is slightly higher.
Write-Behind (Write-Back) Application writes directly to Redis, and an asynchronous worker queues writes to the database. Ultra-fast writes and batching. Risk of data loss if Redis crashes before async persistence.

2. Eliminating the Cache Stampede (Thundering Herd)

When a heavily requested cached key expires under high concurrency (e.g., 5,000 requests/sec), hundreds of parallel worker threads discover the cache miss at the exact same millisecond. They all simultaneously hit the primary database with identical heavy queries, causing CPU exhaustion and cascading timeouts.

Solution: Probabilistic Early Expiration or Distributed Locks

We can solve this cleanly by acquiring an atomic distributed lock using Redis SET ... NX PX before executing the database fallback:

<?php

namespace App\Services\Cache;

use Illuminate\Support\Facades\Redis;

class ResilientCache
{
    /**
     * Fetch from cache with atomic lock fallback to prevent thundering herd.
     */
    public static function remember(string $key, int $ttlSeconds, callable $callback)
    {
        $cached = Redis::get($key);
        if ($cached !== null) {
            return unserialize($cached);
        }

        $lockKey = "lock:{$key}";
        // Acquire 5-second atomic lock with NX (only if not exists)
        $acquired = Redis::set($lockKey, '1', 'EX', 5, 'NX');

        if ($acquired) {
            try {
                $value = $callback();
                Redis::setex($key, $ttlSeconds, serialize($value));
                return $value;
            } finally {
                Redis::del($lockKey);
            }
        }

        // Another process is calculating the value; sleep 50ms and retry
        usleep(50000);
        return self::remember($key, $ttlSeconds, $callback);
    }
}

3. Selecting the Optimal Eviction Policy (maxmemory-policy)

When Redis reaches its allocated RAM limit (maxmemory 8gb), its eviction policy determines which keys are purged to accommodate new writes. Setting the wrong policy can inadvertently delete sessions or critical state.

# /etc/redis/redis.conf
maxmemory 8gb

# Eviction strategies:
# volatile-lru   -> Evict least recently used keys with an explicit TTL
# allkeys-lru    -> Evict least recently used keys across all keys (Ideal for pure cache)
# volatile-lfu   -> Evict least frequently used keys with TTL (Great for traffic spikes)
# noeviction     -> Return error on write when memory full (Required for queue brokers)

maxmemory-policy volatile-lru
Best Practice: Always separate your Cache Redis instance (configured with allkeys-lru or volatile-lru) from your Queue/Session Redis instance (configured with noeviction). Never mix caching with critical persistence workloads on the same daemon.

4. Scaling with Redis Cluster & Sharding

When dataset size or write throughput exceeds a single node's CPU/RAM limits, Redis Cluster partitions data across multiple master nodes using 16,384 hash slots.

# Slot allocation formula:
HASH_SLOT = CRC16(key) mod 16384

To execute multi-key operations (transactions or pipelines) within Redis Cluster, ensure all related keys route to the same hash slot using Hash Tags:

# Both keys route to the exact same cluster master node via the {user:104} tag
SET {user:104}:profile "{\"name\":\"Sejan\"}"
SET {user:104}:preferences "{\"theme\":\"dark\"}"

5. High Availability with Redis Sentinel

For standalone or primary-replica architectures, Redis Sentinel provides automatic health monitoring, notification, and zero-touch failover:

  • Monitoring: Continuous heartbeat checks between Sentinel nodes and primary/replica Redis daemons.
  • Quorum Consensus: When a majority of Sentinel nodes agree a primary is unreachable (ODOWN), a replica is elected and promoted to primary.
  • Dynamic Client Reconfiguration: Sentinels publish the new primary's IP address to client libraries without restarting web applications.

Summary & Key Architectural Rules

  1. Always set explicit TTLs: Keys without expiration will eventually consume all RAM.
  2. Prevent stampedes: Use distributed mutex locks or background async cache refreshing.
  3. Compress large payloads: Compress JSON blobs exceeding 10KB using Snappy or Gzip before storing in Redis.
  4. Monitor Memory Fragmentation: Regularly inspect mem_fragmentation_ratio via redis-cli info memory.

Comments (0)

Leave a Reply ↓

No comments yet. Be the first to start the discussion!

Leave a Comment