Distributed Caching Patterns & Eviction
1. What It Is & Why It Exists
The Core Problem
Persistent storage engines (relational databases, document stores, and distributed filesystems) are fundamentally bounded by disk I/O, disk seek latencies, B-Tree index locking, and multi-tenant connection pools. Typical database query latency ranges from to over .
In high-throughput distributed applications handling hundreds of thousands of read requests per second, querying disk-bound databases directly causes:
- Connection Pool Exhaustion: Available database worker threads and TCP sockets saturate within milliseconds.
- CPU & Lock Contention: Intensive query parsing, table lock contention, and buffer pool churn degrade database throughput.
- Cascading Latency Cliffs (Tail Latency Explosion): P99 and P99.9 latency spikes exponentially, causing upstream service timeouts, client retry storms, and total system brownouts.
The Breakdown: The Unshielded Database Collapse
Concrete Proof: Unshielded Database vs. Distributed Cache
Consider an e-commerce platform processing during a promotional event, where access follows an 80/20 Pareto distribution (80% of queries target 20% of catalog items):
| Metric / Dimension | Direct Database Architecture (No Cache) | Distributed In-Memory Cache Tier ( Hit Ratio) |
|---|---|---|
| Storage Medium | NVMe SSD / Disk Buffer Pool | Volatile RAM (Redis / Memcached) |
| Access Latency | ( faster) | |
| QPS Capacity per Node | (Database primary) | (In-memory node) |
| Database Load at 200k QPS | 🚨 Connection pool exhausted | 🛡️ ( traffic absorbed in RAM) |
| Database Fleet Sizing | Requires 40–80 read replicas (Cost: >\30,000/\text{mo}$) | Single primary + 2 read replicas (Cost: \approx \2,500/\text{mo}$) |
| P99 Read Latency | 🚨 (Queuing backlog) | ⚡ (Ultra-stable SLA) |
The Database Throughput Wall: Relational databases cannot scale reads indefinitely via read replicas due to primary replication lag, cross-AZ synchronization overhead, and connection limits. Introducing a distributed cache is not an optimization; it is an architectural prerequisite for horizontal scale.
The First-Principles Solution: In-Memory Caching
In-memory caching places high-speed, volatile RAM stores (e.g., Redis, Memcached, DynamoDB DAX) between the application tier and the primary persistence layer. Accessing RAM takes compared to for solid-state disk operations—a speedup of to .
Interactive Architecture DiagramSynthesizing vector architecture diagram...
2. Core Mechanics & Caching Design Patterns
Caching Design Patterns Deep-Dive
1. Cache-Aside (Lazy Loading)
- Read Path: The application queries the cache first. On a cache hit, data is returned immediately. On a cache miss, the application queries the database, writes the retrieved value to the cache with a Time-To-Live (TTL), and returns data to the client.
- Write Path: The application updates the database first, then evicts (deletes) the corresponding key from the cache (
DEL key). - Trade-Offs:
- Pros: Memory-efficient (only queried data is cached); resilient to cache node failures (cache misses gracefully fall back to the DB).
- Cons: Initial read experiences cache miss latency; vulnerable to dual-write race conditions if not paired with cache eviction.
2. Write-Through
- Mechanics: The application writes directly to the caching layer. The cache synchronously writes data to the underlying database before returning success to the client.
- Trade-Offs:
- Pros: Cache is never stale; subsequent reads are guaranteed hits.
- Cons: High write latency (incurs RAM + DB round-trip latency); pollutes cache with data that may never be read again.
3. Write-Behind (Write-Back)
- Mechanics: The application writes directly to the cache, which immediately acknowledges success (). An asynchronous background worker batches dirty cache entries and writes them to the database periodically.
- Trade-Offs:
- Pros: Extreme write throughput; absorbs write bursts and collapses multiple writes to the same key into a single DB write.
- Cons: High risk of data loss—if the cache node crashes before flushing dirty pages, uncommitted updates are permanently lost.
4. Refresh-Ahead (Proactive Caching)
- Mechanics: The cache framework analyzes access frequency and automatically reloads keys from the database before their TTL expires.
- Trade-Offs:
- Pros: Eliminates cache miss latency for hot keys.
- Cons: Mispredicting access patterns causes unnecessary database load refreshing cold keys.
Interactive Architecture DiagramSynthesizing vector architecture diagram...
Formal Mathematical Formulations
1. Effective Access Time (EAT)
The performance of a tiered memory architecture is governed by its Cache Hit Ratio ():
Where is in-memory retrieval latency () and is primary database query latency ().
The 90% vs 99% Rule:
- At : .
- At : ( lower average latency).
2. Probabilistic Early Expiration (The XFetch Algorithm)
To prevent the Cache Stampede without distributed locks, the XFetch algorithm computes an asynchronous early refresh decision dynamically during read queries:
Where:
- (time remaining until expiration).
- is the observed execution time required to compute/fetch the item from the database.
- is the aggressiveness multiplier (typically ).
- is a random floating-point value.
- As , the probability of triggering an asynchronous background refresh approaches , guaranteeing that at least one client thread refreshes the cache before expiration.
Eviction Policies & In-Memory Data Structures
When cache memory reaches its configured maxmemory limit, the eviction policy determines which keys are purged:
Interactive Architecture DiagramSynthesizing vector architecture diagram...
In-Memory Node Layout
A production LRU node inside a doubly-linked list contains pointers and payload metadata:
| Node Field | Type / Size | Purpose |
|---|---|---|
prev | uint64_t* (8 Bytes) | Pointer to previous node toward Head |
next | uint64_t* (8 Bytes) | Pointer to next node toward Tail |
key | char* (8 Bytes) | Pointer to cached key string |
val | void* (8 Bytes) | Pointer to serialized payload value |
expires_at | int64_t (8 Bytes) | Unix timestamp for TTL expiration |
| Total Overhead | 40 Bytes | Per-entry node overhead (excluding jemalloc chunk padding) |
State-Transition Trace: LRU Doubly-Linked List Eviction
Under capacity limit entries, with initial state [C, B, A] (Head = C [MRU], Tail = A [LRU]):
| Step # | Event / Input | In-Memory / Distributed State | Evaluation & Transition | Outcome / Cache Latency |
|---|---|---|---|---|
| Initial | Baseline state | Capacity , Full Map: {A, B, C} | Pointers: HEAD <-> [C] <-> [B] <-> [A] <-> TAILHead = C (MRU), Tail = A (LRU) | Cache full ( slots allocated) |
| 1 | GET B (Read Hit) | Hash map lookup finds B pointer in | Unlink B.prev & B.next; re-splice B to HeadOrder becomes [B <-> C <-> A]; Tail A unchanged | Cache Hit ()B promoted to MRU; A remains LRU |
| 2 | SET D = "val_d" (Write Insert) | Key D absent; capacity ceiling reached () | Evict Tail (A): delete map["A"], free node AAllocate node D, splice as new Head | Eviction + Insert () Order: [D <-> B <-> C]; C is now LRU Tail |
| 3 | SET C = "v2" (Update Hit) | Key C found at Tail position | Update value pointer C.val = "v2"; unlink from TailSplice C to Head; B becomes new Tail | Update + Promotion () Order: [C <-> D <-> B]; B is now LRU Tail |
Eviction Algorithm Comparison Matrix
| Eviction Algorithm | Underlying Data Structure | Time Complexity | Memory per Key | Vulnerability / Failure Mode | Real-World Adoption |
|---|---|---|---|---|---|
| LRU (Least Recently Used) | Doubly-Linked List + Hash Map | get/put | Scan Resistance Failure: A full table batch scan flushes entire working set. | Redis allkeys-lru, Memcached | |
| LFU (Least Frequently Used) | Frequency Buckets + Linked Lists | get/put | Historical Bias: Historical keys accumulate millions of hits and resist eviction long after going cold. | Redis allkeys-lfu | |
| FIFO (First In, First Out) | Queue / Circular Ring Buffer | Sub-optimal hit ratio; evicts hot items simply due to age. | Varnish, Simple streaming buffers | ||
| Random Eviction | Reservoir / Uniform Sampling | High variance; randomly evicts critical hot keys. | Redis allkeys-random | ||
| W-TinyLFU | SLRU + Count-Min Sketch Filter | Minor CPU overhead on write to update frequency sketch. | Caffeine (Java), Ristretto (Go) |
3. Data Migration, Anti-Entropy & Consistency Protocols
Maintaining consistency between a primary relational database and an external caching tier across distributed networks requires addressing the Dual-Write Concurrency Problem:
Interactive Architecture DiagramSynthesizing vector architecture diagram...
The Invalidation Rule: Never Update, Always Invalidate
To prevent race conditions where out-of-order network packets store stale data in cache indefinitely, never write updated entities directly into the cache. Always execute an atomic Cache Invalidation (DEL key):
Asynchronous Anti-Entropy via Change Data Capture (CDC)
In microservice architectures, application code may crash between updating the database and evicting the cache. To achieve guaranteed eventual consistency, production systems decouple cache invalidation from application logic using CDC (Change Data Capture):
Interactive Architecture DiagramSynthesizing vector architecture diagram...
- Transactional Guarantee: The application writes only to the database. The database writes changes atomically to its append-only transaction log (WAL / MySQL Binlog).
- Streaming Invalidation: A CDC connector (e.g., Debezium) streams change events to Apache Kafka.
- Consumer Eviction: Dedicated invalidator workers consume events and execute
DEL {tenant}:user:{id}against Redis clusters, guaranteeing cache invalidation even if application instances crash.
3. Online Cluster Resharding, Anti-Entropy & Graceful Draining
- Slot Migration Protocol: Redis Cluster distributes 16,384 slots across primary nodes. During dynamic scaling:
- Set target shard state:
CLUSTER SETSLOT <slot> IMPORTING <source_node_id>. - Set source shard state:
CLUSTER SETSLOT <slot> MIGRATING <target_node_id>. - Batch migrate keys atomically:
MIGRATE <target_ip> <port> "" 0 5000 KEYS <key1> <key2>.... - Commit slot assignment: broadcast
CLUSTER SETSLOT <slot> NODE <target_node_id>to all cluster nodes.
- Set target shard state:
- Client Redirection Handling (
ASKvs.MOVED): In-flight keys on migrating slots return-ASK <slot> <target_ip:port>. The client issues anASKINGprefix command to the target node for that single query without updating its local slot routing cache. Once the slot fully commits, queries return-MOVED <slot> <target_ip:port>, prompting clients to refresh their cluster topology map asynchronously. - Replica Synchronization & Draining: Replicas synchronize state via an in-memory ring buffer (
repl-backlog). On brief network blips, partial resynchronization (PSYNC <replication_id> <offset>) avoids full RDB disk dumps. During node decommissioning, operators triggerCLUSTER FAILOVER TAKEOVERto elevate the replica gracefully and drain existing client TCP connections with zero data loss.
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~44%). Spend 1 Coin to unlock the remaining 7 production deep-dive sections for a full 24 hours.