Chapter 65: Design Distributed Cache (Redis Cluster)
1. Requirements
Functional Requirements
- GET/SET Operations: Store and retrieve key-value pairs
- Data Structures: Support strings, lists, sets, sorted sets, hashes
- TTL Support: Automatic expiration of keys
- Atomic Operations: INCR, DECR, compare-and-swap operations
- Partitioning: Distribute data across multiple nodes
- Replication: Data redundancy for high availability
Non-Functional Requirements
- Latency: Sub-millisecond read/write latency (p99 < 1ms)
- Throughput: Handle millions of operations per second
- Availability: 99.99% uptime with automatic failover
- Scalability: Horizontal scaling by adding nodes
- Consistency: Eventual consistency for replicas, linearizable for primary
- Durability: Optional persistence (RDB snapshots, AOF logs)
Scale Estimation
Average key size: 50 bytes
Average value size: 1 KB
Total keys: 1 billion keys
Storage: ~1 TB in-memory data
Cluster size: 50 nodes (20 GB per node)
Replication factor: 3 (1 primary + 2 replicas)
2. High-Level Design
Port 7000] R1A[Replica 1A
Port 7001] R1B[Replica 1B
Port 7002] end subgraph Shard 2 - Slots 5461-10922 M2[Master 2
Port 7003] R2A[Replica 2A
Port 7004] R2B[Replica 2B
Port 7005] end subgraph Shard 3 - Slots 10923-16383 M3[Master 3
Port 7006] R3A[Replica 3A
Port 7007] R3B[Replica 3B
Port 7008] end end Client1 --> M1 Client2 --> M2 Client3 --> M3 M1 -.->|Async Repl| R1A M1 -.->|Async Repl| R1B M2 -.->|Async Repl| R2A M2 -.->|Async Repl| R2B M3 -.->|Async Repl| R3A M3 -.->|Async Repl| R3B M1 <-.->|Gossip| M2 M2 <-.->|Gossip| M3 M3 <-.->|Gossip| M1
Core Components
- Master Nodes: Handle reads and writes for assigned hash slots
- Replica Nodes: Async copies of master data, promote on master failure
- Hash Slots: 16384 slots, evenly distributed across masters
- Gossip Protocol: Nodes exchange cluster state (topology, health)
- Client Library: Smart client caches slot-to-node mapping
Why 16384 Slots?
- Trade-off between granularity (migration flexibility) and overhead (heartbeat size)
- 16384 = 2^14, fits in 2 KB bitmap for heartbeat messages
- Allows fine-grained resharding without excessive metadata
3. Hash Slot Assignment
CRC16 Hashing
Redis Cluster uses CRC16 to map keys to hash slots:
slot = CRC16(key) mod 16384
// Example:
key = "user:1234:session"
CRC16("user:1234:session") = 52341
slot = 52341 % 16384 = 3189
Hash Tags for Multi-Key Operations
Multi-key operations (MGET, MSET) require all keys to be on the same slot. Use hash tags:
// Without hash tag (different slots):
user:1234:name -> slot 7234
user:1234:email -> slot 9012
user:1234:session -> slot 3189
// With hash tag (same slot):
user:{1234}:name -> slot based on "1234"
user:{1234}:email -> slot based on "1234"
user:{1234}:session -> slot based on "1234"
// Hash tag rule: Use substring between first { and first }
Slot Distribution Example
| Node | Slots | Count | Percentage |
|---|---|---|---|
| Master 1 | 0 - 5460 | 5461 | 33.3% |
| Master 2 | 5461 - 10922 | 5462 | 33.3% |
| Master 3 | 10923 - 16383 | 5461 | 33.3% |
4. Data Partitioning Strategies
Client-Side Partitioning
Client knows the full cluster topology and routes requests directly to the correct node.
Pros: No extra hop, lowest latency
Cons: Client must maintain topology, handle resharding
Proxy-Based Partitioning
Proxy layer (Twemproxy, Envoy) handles routing. Client unaware of cluster topology.
Pros: Dumb client, centralized logic
Cons: Extra hop (latency), proxy is bottleneck/SPOF
Redis Cluster Mode (Hybrid)
Client initially connects to any node. If wrong node, receives redirect (MOVED/ASK).
Pros: Eventually optimal routing, no proxy overhead
Cons: Initial redirects, client must handle MOVED/ASK
5. Replication
Asynchronous Replication
Redis uses async replication for performance. Master doesn't wait for replica ACKs.
// Master flow:
1. Client sends SET key value
2. Master applies write to in-memory store
3. Master returns OK to client immediately
4. Master async sends write to replicas
5. Replicas apply write and ACK (master doesn't wait)
Replication Flow
Replica Promotion (Failover)
When master fails, cluster automatically promotes a replica to master.
- Failure Detection: Nodes ping each other via gossip. If majority marks master as FAIL, trigger failover
- Replica Election: Replicas with most up-to-date replication offset compete
- Vote Process: Replicas request votes from other masters
- Promotion: First replica to get majority votes becomes new master
- Topology Update: New master takes over hash slots, broadcasts via gossip
- Client Update: Clients receive MOVED redirects to new master
Replication Lag
- Replication Offset: Master tracks byte position in replication stream
- Monitoring:
INFO replicationshows master_repl_offset vs replica offset - Mitigation: If lag > threshold, alert ops team; consider sync writes (WAIT command)
6. Cluster Communication (Gossip Protocol)
Gossip Protocol Purpose
- Topology Discovery: Each node learns about all other nodes
- Health Monitoring: Detect node failures via heartbeats
- Slot Mapping: Propagate which master owns which slots
- Configuration Changes: Broadcast resharding, promotions
Gossip Message Format
// Heartbeat message every 1 second
{
"sender": "node_id_1",
"slots": [0, 1, 2, ..., 5460], // Bitmap of owned slots
"nodes": [
{"id": "node_id_1", "ip": "10.0.0.1", "port": 7000, "flags": "master"},
{"id": "node_id_2", "ip": "10.0.0.2", "port": 7001, "flags": "replica"},
{"id": "node_id_3", "ip": "10.0.0.3", "port": 7002, "flags": "master,fail"}
],
"epoch": 42 // Configuration version
}
Failure Detection
- PFAIL (Possible Fail): Node A doesn't receive heartbeat from Node B for NODE_TIMEOUT (default 15s). A marks B as PFAIL locally.
- FAIL (Definite Fail): If majority of masters mark B as PFAIL, they broadcast FAIL. All nodes mark B as FAIL.
- Failover Trigger: B's replicas detect FAIL, start election
Split Brain Prevention
- Epoch (Configuration Version): Each config change increments epoch
- Majority Quorum: Failover requires majority of masters to agree
- Stale Config Rejection: Nodes reject commands from nodes with lower epoch
7. Client Routing and Redirects
Smart Client with Caching
Redis clients cache the slot-to-node mapping to route requests directly.
// Client-side slot mapping cache
slot_map = {
0-5460: "10.0.0.1:7000",
5461-10922: "10.0.0.2:7003",
10923-16383: "10.0.0.3:7006"
}
// Client logic:
def get(key):
slot = crc16(key) % 16384
node = slot_map.lookup(slot)
return node.get(key)
MOVED Redirect
Sent when client contacts wrong node for a slot (after resharding).
// Client sends to wrong node:
Client -> Master1: GET user:5678
Master1 -> Client: -MOVED 12000 10.0.0.3:7006
// Client updates cache and retries:
Client updates slot_map[12000] = "10.0.0.3:7006"
Client -> Master3: GET user:5678
Master3 -> Client: "Alice"
ASK Redirect
Sent during resharding when slot is being migrated. Temporary redirect.
// Slot 5000 is migrating from Master1 to Master2
Client -> Master1: GET key_in_slot_5000
// If key not yet migrated:
Master1 -> Client: value
// If key already migrated:
Master1 -> Client: -ASK 5000 10.0.0.2:7003
// Client sends ASKING + retry (doesn't update cache)
Client -> Master2: ASKING
Client -> Master2: GET key_in_slot_5000
Master2 -> Client: value
Difference: MOVED vs ASK
| Aspect | MOVED | ASK |
|---|---|---|
| Meaning | Slot permanently moved | Slot being migrated |
| Client Action | Update slot map cache | Do NOT update cache |
| Retry | Direct retry to new node | Send ASKING flag first |
| Duration | Permanent | Temporary (until migration done) |
8. Eviction Policies
When to Evict
When memory usage reaches maxmemory limit, Redis evicts keys based on policy.
Eviction Policies
| Policy | Description | Use Case |
|---|---|---|
| noeviction | Return error on writes | Never lose data |
| allkeys-lru | Evict least recently used | General purpose cache |
| allkeys-lfu | Evict least frequently used | Workloads with few hot keys |
| volatile-lru | LRU among keys with TTL | Mix of cache + persistent data |
| volatile-lfu | LFU among keys with TTL | Frequency-based TTL keys |
| volatile-ttl | Evict shortest TTL first | Expire-sensitive data |
| volatile-random | Random among TTL keys | Simple, fair eviction |
| allkeys-random | Random among all keys | Testing, uniform access |
LRU Implementation Details
- Approximate LRU: Redis doesn't maintain a true LRU linked list (too expensive)
- Sampling: Sample 5 random keys (configurable), evict the least recently used among them
- LRU Clock: 24-bit timestamp per key (last access time, ~194 days overflow)
LFU Implementation
- Access Counter: 8-bit logarithmic counter (saturates at 255)
- Decay: Counter decays over time to handle changing access patterns
- Formula: Counter increases with probability 1 / (old_counter * lfu_log_factor)
9. Hot Key Mitigation
Hot Key Problem
A single key receiving millions of requests can overwhelm one master node (since Redis is single-threaded).
- Example: Celebrity profile cache, viral post counter
- Symptom: High CPU on one master, uneven load across cluster
Mitigation Strategies
1. Local Client-Side Cache
// Local cache in application memory
local_cache = {}
def get(key):
if key in local_cache and not expired(local_cache[key]):
return local_cache[key].value
value = redis_cluster.get(key)
local_cache[key] = {"value": value, "ttl": 60}
return value
Pros: No Redis load, sub-microsecond latency
Cons: Stale data, memory per app instance
2. Key Splitting (Hash Tag Trick)
// Split hot key across multiple slots
// Instead of: user:celebrity:followers -> single slot
// Use: user:celebrity:followers:{0}, user:celebrity:followers:{1}, ...
def incr_followers(celebrity_id):
shard = random.randint(0, 9) // 10 shards
key = f"user:{celebrity_id}:followers:{{{shard}}}"
redis.incr(key)
def get_followers(celebrity_id):
total = 0
for shard in range(10):
key = f"user:{celebrity_id}:followers:{{{shard}}}"
total += int(redis.get(key) or 0)
return total
Pros: Distributes load across nodes
Cons: Aggregation overhead, not atomic
3. Read Replicas
// Route hot key reads to replicas (eventual consistency ok)
def get_read_heavy_key(key):
slot = crc16(key) % 16384
master = slot_map[slot]
replicas = get_replicas(master)
# Round-robin across replicas
replica = replicas[random.choice(len(replicas))]
return replica.get(key)
4. In-Memory Multi-Threaded Proxy
- Deploy proxy (e.g., Twemproxy with local cache)
- Proxy absorbs hot key reads, forwards writes to Redis
- Trade-off: Stale reads, added complexity
redis-cli --hotkeys or custom latency tracking. Set up alerts when a key exceeds QPS threshold.
10. Consistency Model
Single-Master Consistency
All writes for a hash slot go to one master - linearizable within a shard.
// Client 1 writes, Client 2 reads from same master:
Client1 -> Master1: SET x 10
Master1 -> Client1: OK
Client2 -> Master1: GET x
Master1 -> Client2: 10 // Guaranteed to see latest write
Eventual Consistency (Replicas)
Replicas lag behind master due to async replication.
// Write to master, read from replica:
Client1 -> Master1: SET x 10
Master1 -> Client1: OK
// Replication lag (e.g., 100ms)
Client2 -> Replica1: GET x
Replica1 -> Client2: (old value or key not found)
// After replication:
Client2 -> Replica1: GET x
Replica1 -> Client2: 10
Read-Your-Writes Consistency
Ensure user sees their own writes by routing their reads to master or using sticky sessions.
- Sticky to Master: User's reads go to master for TTL duration after write
- Version Tracking: Tag write with version, read from replica only if version caught up
WAIT Command (Semi-Sync)
// Block until N replicas acknowledge write
SET key value
WAIT 2 1000 // Wait for 2 replicas, timeout 1000ms
// Use case: Critical writes (payment, inventory)
// Trade-off: Latency increase (p99 can double)
Cluster-Wide Consistency
- No Cross-Shard Transactions: Redis Cluster doesn't support multi-key transactions across shards
- Workaround: Use hash tags to colocate related keys on same shard
- Lua Scripts: Atomic operations on same shard via
EVAL
11. Scalability and Resharding
Adding Nodes to Cluster
- Add Empty Node: Start new Redis instance, join to cluster
- Reshard Slots: Move some hash slots from existing masters to new node
- Migrate Keys: For each slot, iterate keys and move them
- Update Topology: Broadcast new slot mapping via gossip
Online Resharding Process
Resharding Commands
// Step 1: Mark slot as migrating
redis-cli --cluster reshard 127.0.0.1:7000
--cluster-from node_id_1
--cluster-to node_id_4
--cluster-slots 1000
// Step 2: Migrate keys
CLUSTER GETKEYSINSLOT slot count // Get keys in slot
MIGRATE target_ip target_port key 0 timeout // Move key
// Step 3: Finalize
CLUSTER SETSLOT slot NODE node_id_4 // Transfer ownership
Handling Resharding in Clients
- ASK Redirects: Client gets ASK for migrating slots, retries with ASKING flag
- MOVED Redirects: After migration completes, client gets MOVED, updates slot map
- Minimal Downtime: Keys still accessible during migration (split between old and new master)
Removing Nodes
- Reshard slots away from node to be removed
- Wait for all slots to be empty (0 keys)
- Remove node from cluster:
CLUSTER FORGET node_id - Shutdown node
Scaling Reads vs Writes
| Workload | Scaling Strategy |
|---|---|
| Read-Heavy | Add more replicas per master (2 -> 5 replicas) Route reads to replicas |
| Write-Heavy | Add more master nodes (shards) Reshard to distribute write load |
| Balanced | Add master+replica pairs Reshard evenly across new masters |
Monitoring and Observability
- Metrics: Ops/sec, latency (p50/p99), memory usage, eviction rate, replication lag
- Cluster Health:
CLUSTER INFO- cluster_state, cluster_slots_ok, cluster_known_nodes - Slow Log:
SLOWLOG GET- queries exceeding threshold (default 10ms) - Key Distribution:
redis-cli --cluster check- verify even key distribution
Summary
Designing a distributed cache like Redis Cluster requires careful consideration of:
- Hash Slot Partitioning: 16384 slots with CRC16 hashing for even, predictable distribution
- Async Replication: Performance-first approach with replica promotion for high availability
- Gossip Protocol: Decentralized cluster communication for topology and failure detection
- Client Routing: Smart clients with MOVED/ASK redirects for efficient request routing
- Eviction Policies: LRU/LFU to stay within memory bounds
- Hot Key Mitigation: Local cache, key splitting, read replicas
- Eventual Consistency: Trade-off for extreme performance, use WAIT for critical writes
- Online Resharding: Add/remove nodes without downtime via slot migration
Redis Cluster achieves sub-millisecond latency at massive scale by prioritizing performance over strong consistency and using specialized data structures optimized for in-memory operations.