Consistent Hashing Cache Shard in Python
A minimal consistent hashing ring with virtual nodes that distributes cache keys across shards and minimizes re-mapping when a node is removed.
Python code
48 linesimport hashlib
import bisect
class ConsistentHashRing:
def __init__(self, nodes=None, replicas=3):
self.replicas = replicas
self.ring = {}
self.sorted_keys = []
if nodes:
for node in nodes:
self.add_node(node)
def _hash(self, key):
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node):
for i in range(self.replicas):
key = self._hash(f"{node}:{i}")
self.ring[key] = node
bisect.insort(self.sorted_keys, key)
def remove_node(self, node):
for i in range(self.replicas):
key = self._hash(f"{node}:{i}")
del self.ring[key]
idx = bisect.bisect_left(self.sorted_keys, key)
self.sorted_keys.pop(idx)
def get_node(self, key):
if not self.sorted_keys:
return None
hash_key = self._hash(key)
idx = bisect.bisect_left(self.sorted_keys, hash_key)
if idx == len(self.sorted_keys):
idx = 0
return self.ring[self.sorted_keys[idx]]
if __name__ == "__main__":
ring = ConsistentHashRing(["cache-1", "cache-2", "cache-3"])
data_keys = ["user:42", "order:1001", "product:7", "cart:99", "session:abc"]
assignments = {k: ring.get_node(k) for k in data_keys}
print("Initial assignments:", assignments)
ring.remove_node("cache-2")
new_assignments = {k: ring.get_node(k) for k in data_keys}
print("After removing cache-2:", new_assignments)
Output
Initial assignments: {'user:42': 'cache-3', 'order:1001': 'cache-1', 'product:7': 'cache-2', 'cart:99': 'cache-2', 'session:abc': 'cache-1'}
After removing cache-2: {'user:42': 'cache-3', 'order:1001': 'cache-1', 'product:7': 'cache-1', 'cart:99': 'cache-1', 'session:abc': 'cache-1'}
How it works
The ring maps hash values to nodes using MD5 to generate a 128-bit integer. Each node gets replicas virtual positions (node:0, node:1, etc.) to spread keys evenly. bisect_left finds the first key at or after the data key's hash, wrapping to the start of the ring when reaching the end. Removing a node deletes all its virtual points, and only keys whose hash lands on those points move to the next node — most keys stay put. Add remove_node is O(k log n) where k is the replica count, so the ring stays cheap to update even with hundreds of nodes.
Common mistakes
- Using MD5 in security-sensitive contexts (it's fine for sharding but not cryptography)
- Forgetting to wrap around the ring when bisect returns len(keys)
- Using too few replicas (e.g., 1) causing unbalanced distributions
Variations
- Use sha256 instead of md5 by swapping the hash function
- Return a sorted list of nodes with the ring for weighted consistency hashing
Real-world use cases
- Sharding Redis or Memcached keys across a pool of cache instances so each key always lands on the same node.
- Distributing user sessions across app server instances in a load balancer without a central session store.
- Mapping file chunks or database rows to partition nodes in a distributed storage system while tolerating node additions.
Sponsored
More from Caching & Redis
- Cache Asides in Python with a Read-Through Loader easy
- Cache Data in Redis with Python easy
- Cache Penetration Null Object Mock in Python medium
- Cache Stampede Prevention with SingleFlight in Python medium
- Cache Warming with Python: Preload Hot Keys easy
- Coalescing duplicate in-flight requests: one shared result for concurrent callers hard
Keep learning
Related tutorials and quizzes for this topic.