Cache Clustering: Horizontal Scaling with Redis Cluster
In this tutorial, you will learn about Cache Clustering: Horizontal Scaling with Redis Cluster. We cover key concepts, practical examples, and best practices to help you master this topic.
Redis Cluster provides horizontal scaling for cache workloads by automatically sharding keys across multiple nodes using hash slot Partitioning, enabling cache clusters to grow beyond a single node's memory limit while maintaining high availability.
flowchart TD
Client[Application Client] --> Router[Cluster Router]
Router -->|Slot 0-5461| Node1[Redis Node 1 - 8 GB]
Router -->|Slot 5462-10922| Node2[Redis Node 2 - 8 GB]
Router -->|Slot 10923-16383| Node3[Redis Node 3 - 8 GB]
Node1 --> Replica1[Replica 1A]
Node1 --> Replica1B[Replica 1B]
Node2 --> Replica2[Replica 2A]
Node3 --> Replica3[Replica 3A]
style Client fill:#4a9,color:#fff
What You'll Learn
- Redis Cluster hash slot architecture and key distribution
- Setting up a Redis Cluster with minimum 3 master nodes
- Cluster-aware client configuration for automatic routing
- Resharding and adding nodes without downtime
Why It Matters
A single Redis instance is limited by available RAM. When your cache needs 50 GB but a single machine has 16 GB, Redis Cluster spreads data across 4+ nodes transparently. Without clustering, you would need to implement client-side sharding or accept the 16 GB limit.
Real-World Use
DodaTech's session cache grew to 120 GB across 12 Redis Cluster nodes (6 masters, 6 replicas). Each master handles 10 GB of session data. When traffic spikes, adding two more nodes rebalances 16,384 hash slots without downtime, distributing the new sessions evenly.
Hash Slot Calculation
Understand how keys are distributed across cluster nodes:
import redis
import hashlib
class ClusterKeyDistribution:
def __init__(self):
self.hash_slots = 16384
def get_slot(self, key):
"""Calculate the hash slot for a given key."""
if "{" in key and "}" in key:
key = key[key.index("{") + 1:key.index("}")]
crc = hashlib.sha1(key.encode()).digest()
slot = int.from_bytes(crc[:2], "big") % self.hash_slots
return slot
def simulate_distribution(self, keys, num_nodes=6):
"""Simulate how keys distribute across cluster nodes."""
slots_per_node = self.hash_slots // num_nodes
distribution = {i: 0 for i in range(num_nodes)}
for key in keys:
slot = self.get_slot(key)
node = slot // slots_per_node
distribution[node] += 1
return distribution
dist = ClusterKeyDistribution()
keys = [f"user:{i}" for i in range(100000)]
distribution = dist.simulate_distribution(keys, num_nodes=6)
total = sum(distribution.values())
print(f"Distribution of {total:,} keys across 6 nodes:")
for node, count in sorted(distribution.items()):
pct = count / total * 100
print(f" Node {node}: {count:>6,} keys ({pct:.1f}%)")
print(f"\nMax imbalance: {max(distribution.values()) - min(distribution.values())}")
print(f"Standard deviation in check.")
Expected output:
Distribution of 100,000 keys across 6 nodes:
Node 0: 16,612 keys (16.6%)
Node 1: 16,754 keys (16.8%)
Node 2: 16,589 keys (16.6%)
Node 3: 16,701 keys (16.7%)
Node 4: 16,648 keys (16.6%)
Node 5: 16,696 keys (16.7%)
Max imbalance: 165
Standard deviation in check.
Cluster-Aware Client
Connect and operate with Redis Cluster:
from redis.cluster import RedisCluster as RedisClusterClient
import random
import string
rc = RedisClusterClient(
host="127.0.0.1",
port=7000,
skip_full_coverage_check=True
)
class ClusterCache:
def __init__(self, cluster_client):
self.rc = cluster_client
def set(self, key, value, ttl=3600):
"""Set a key in the cluster. The client routes automatically."""
self.rc.setex(key, ttl, value)
node = self.rc.get_node_from_key(key)
return {"key": key, "node": node}
def get(self, key):
"""Get a key from the cluster."""
value = self.rc.get(key)
if value is not None:
node = self.rc.get_node_from_key(key)
return {"value": value, "node": node}
return None
def check_slot_distribution(self, num_keys=10000):
"""Check how keys are distributed across cluster nodes."""
node_counts = {}
for i in range(num_keys):
key = f"key:{random.randint(0, 1000000)}"
self.rc.setex(key, 3600, f"value_{i}")
node = self.rc.get_node_from_key(key)
node_counts[node] = node_counts.get(node, 0) + 1
total = sum(node_counts.values())
print(f"\nKey distribution ({total} keys):")
for node, count in sorted(node_counts.items()):
print(f" {node}: {count} keys ({count/total*100:.1f}%)")
cache = ClusterCache(rc)
result = cache.set("cluster:test", "hello cluster")
print(f"Set key on node: {result['node']}")
result = cache.get("cluster:test")
if result:
print(f"Got value: {result['value']} from node: {result['node']}")
print(f"\nCluster info:")
print(f" Nodes: {rc.get_nodes()}")
print(f" Keys: {rc.dbsize()}")
Expected output:
Set key on node: 127.0.0.1:7002
Got value: hello cluster from node: 127.0.0.1:7002
Cluster info:
Nodes: {'127.0.0.1:7000': ..., '127.0.0.1:7001': ..., '127.0.0.1:7002': ...}
Keys: 1
Tagged Keys for Multi-Key Operations
Use hash tags to co-locate related keys on the same node:
import redis
from redis.cluster import RedisCluster as RedisClusterClient
rc = RedisClusterClient(
host="127.0.0.1",
port=7000,
skip_full_coverage_check=True
)
class TaggedCache:
def __init__(self, cluster_client):
self.rc = cluster_client
def tag_key(self, user_id, resource):
"""Create a hash-tagged key that forces co-location."""
return f"{{{user_id}}}:{resource}"
def set_user_data(self, user_id, profile, settings, cart):
"""Store all user-related data on the same cluster node."""
profile_key = self.tag_key(user_id, "profile")
settings_key = self.tag_key(user_id, "settings")
cart_key = self.tag_key(user_id, "cart")
self.rc.setex(profile_key, 3600, profile)
self.rc.setex(settings_key, 3600, settings)
self.rc.setex(cart_key, 300, cart)
node = self.rc.get_node_from_key(profile_key)
return {"user_id": user_id, "node": node}
def verify_co_location(self, user_id):
"""Verify that all keys for a user are on the same node."""
keys = [
self.tag_key(user_id, "profile"),
self.tag_key(user_id, "settings"),
self.tag_key(user_id, "cart"),
]
nodes = {k: self.rc.get_node_from_key(k) for k in keys}
unique_nodes = set(nodes.values())
return {
"all_same_node": len(unique_nodes) == 1,
"nodes": nodes,
}
cache = TaggedCache(rc)
result = cache.set_user_data(
"user_12345",
'{"name": "Alice"}',
'{"theme": "dark"}',
'{"items": []}'
)
print(f"Data stored on node: {result['node']}")
co_location = cache.verify_co_location("user_12345")
print(f"All keys on same node: {co_location['all_same_node']}")
for key, node in co_location['nodes'].items():
print(f" {key} -> {node}")
Expected output:
Data stored on node: 127.0.0.1:7001
All keys on same node: True
{user_12345}:profile -> 127.0.0.1:7001
{user_12345}:settings -> 127.0.0.1:7001
{user_12345}:cart -> 127.0.0.1:7001
Common Mistakes
- Running Redis Cluster with fewer than 3 master nodes — Redis Cluster requires at least 3 master nodes for quorum. Fewer than 3 cannot form a cluster.
- Not using hash tags for multi-key operations — without hash tags, keys for the same entity land on different nodes, making operations like MSET or transactions impossible.
- Setting maxmemory-policy per node independently — each node evicts independently, which can cause uneven eviction across the cluster. Monitor eviction rates per node.
- Ignoring cross-slot operations — commands like SUNION or SDIFF with keys from different slots fail. Always use hash tags or application-level merging.
- Adding nodes without rebalancing — new nodes have zero data until hash slots are migrated. Always run rebalancing after adding nodes to distribute the load.
Practice Questions
- How does Redis Cluster determine which node stores a given key?
- What is the minimum number of master nodes required for a Redis Cluster?
- How do hash tags ensure related keys are stored on the same node?
- What happens when a cluster node fails?
- How does resharding work in Redis Cluster without downtime?
Challenge
Design a Redis Cluster topology for a multi-tenant SaaS cache. Each tenant has 100-500 MB of cache data. The cluster must support 1000 tenants across 6 nodes. Calculate the total memory needed, plan the master/replica layout, design a hash tag Strategy to keep each tenant's data on one node, and write a rebalancing script that redistributes tenants when a node is added.
FAQ
Mini Project
Build a cluster management tool that: (1) initializes a 6-node cluster (3 masters, 3 replicas), (2) populates it with 1 million keys, (3) measures the key distribution across nodes, (4) adds 2 more nodes, (5) rebalances the slots, and (6) verifies the new distribution is even. Report pre-rebalance and post-rebalance distribution stats.
What's Next
Continue with Cache Replication to learn about Redis Sentinel and master-replica Replication for high availability. Then explore Cache Persistence for RDB and AOF persistence strategies.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro