Distributed Caching, Redis Internals & Eviction
How Redis handles 100,000+ commands/sec on a single thread, scales horizontally across 16,384 Hash Slots, guarantees persistence with RDB & AOF, and evicts cold data using O(1) LRU & LFU algorithms.
๐ข 1. Why One Redis Server is Never Enough
A single Redis instance is like a lightning-fast supermarket cashier who can scan 100,000 items per second.
However, a single server has physical hardware limits:
1. Memory Limit: A standard cloud server typically caps out at 64 GB to 128 GB of RAM. If your application needs to cache 1 Terabyte of active user sessions, one machine cannot hold it.
2. Throughput Limit: At peak Black Friday traffic (500,000 queries/sec), a single CPU core network stack saturates.
To break through these physical limits, we need Distributed Caching (Redis Cluster):
Automatic data sharding across 16,384 hash slots. Rich data structures (Strings, Hashes, Lists, Sets, Sorted Sets, HyperLogLog, Streams). Built-in master-replica failover.
Multi-threaded key-value store with no disk persistence. The client library hashes keys locally to choose which server to talk to. Simple, fast, but lacks data types and replication.
โก 2. Redis Internals: The Single-Threaded Event Loop
Almost every candidate in system design interviews asks: "Why is Redis single-threaded if modern CPUs have 64 cores?"
Redis creator Salvatore Sanfilippo chose a single-threaded execution model for three brilliant architectural reasons:
epoll() system call. A single thread listens to 100,000 open TCP sockets simultaneously. When a client sends a command, epoll alerts Redis instantly to execute it from RAM in sub-microsecond time!KEYS * or FLUSHALL on a database with 20M keys will block the single thread for 8 seconds. During those 8 seconds, EVERY other user request will time out! Always use cursor-based SCAN in production.๐ฐ 3. Redis Cluster: The 16,384 Hash Slots
How does a Redis Cluster shard data across 3, 10, or 100 master servers without losing keys?
Redis divides the entire universe of keys into exactly 16,384 Hash Slots (numbered 0 to 16,383):
slot = CRC16(key) % 16384{user:101}.profile and {user:101}.orders) to always live on the exact same node for fast multi-key transactions, wrap the shared ID in curly braces: {user:101}. Redis will hash only the text inside the braces!๐พ 4. Redis Persistence: RDB Snapshots vs AOF Logs
RAM is volatile. If power cuts out, all data in memory vanishes. Redis offers two distinct mechanisms to persist data to disk:
Redis forks a child process using Linux Copy-On-Write (COW) to dump the entire memory to a compact binary file (e.g. dump.rdb) every 5 minutes.
Redis logs every single write command sequentially to an append-only file on disk (e.g. appendonly.aof) with fsync everysec.
๐๏ธ 5. Cache Eviction Algorithms: LRU & LFU
When memory reaches maxmemory (e.g. 32 GB), Redis must evict older keys to make room for new incoming writes:
Whenever a key is accessed or written, move it to the Head of the list. When memory is full, instantly drop the key at the Tail! Lookup and eviction are both strictly O(1) time.
Tracks an 8-bit logarithmic access counter for each key with exponential decay. Evicts keys that are rarely accessed, protecting evergreen popular items from being flushed by a temporary batch scan.
๐ป 6. Implementing an O(1) LRU Cache (Multi-Language)
Here is the production data structure implementation of an O(1) LRU Cache using a Doubly-Linked List and Hash Map:
// Python 3.10+ O(1) LRU Cache class Node: def __init__(self, key, val): self.key, self.val = key, val self.prev = self.next = None class LRUCache: def __init__(self, capacity: int): self.cap = capacity self.cache = {} # key -> Node self.head, self.tail = Node(0, 0), Node(0, 0) self.head.next, self.tail.prev = self.tail, self.head def _remove(self, node): node.prev.next = node.next node.next.prev = node.prev def _add_to_head(self, node): node.next = self.head.next node.prev = self.head self.head.next.prev = node self.head.next = node def get(self, key: int) -> int: if key in self.cache: node = self.cache[key] self._remove(node) self._add_to_head(node) return node.val return -1 def put(self, key: int, value: int): if key in self.cache: self._remove(self.cache[key]) node = Node(key, value) self._add_to_head(node) self.cache[key] = node if len(self.cache) > self.cap: lru = self.tail.prev self._remove(lru) del self.cache[lru.key]
๐งช 7. Interactive Lab: Live LRU Eviction & Cluster Router
Interactive LRU Doubly-Linked List & Hash Slot Lab
๐ฏ 8. Test Your Understanding
A nightly analytical job reads 5,000,000 old records through an LRU cache. The next morning, user read latency spikes from 1ms to 40ms because all popular evergreen data was evicted. Which eviction algorithm prevents this?
You need to perform an atomic transaction across two keys (user:101:balance and user:101:history) in a Redis Cluster. Why will standard Redis Cluster return a CROSSSLOT Keys in request don't hash to the same slot error, and how do you fix it?