Skip to main content
Primitives/Primitive #04
PRIMITIVE #04Core Distributed Systems Component

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 2 ms2\text{ ms} to over 50 ms50\text{ ms}.

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): and .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 200,000 read QPS200,000\text{ read QPS} during a promotional event, where access follows an 80/20 Pareto distribution (80% of queries target 20% of catalog items):

Metric / DimensionDirect Database Architecture (No Cache)Distributed In-Memory Cache Tier (95%95\% Hit Ratio)
Storage MediumNVMe SSD / Disk Buffer PoolVolatile RAM ( / Memcached)
Access Latency5.0 ms25.0 ms5.0\text{ ms} - 25.0\text{ ms}0.2 ms1.0 ms0.2\text{ ms} - 1.0\text{ ms} (10×100×10\times\text{--}100\times faster)
Capacity per Node2,0005,000 QPS2,000\text{--}5,000\text{ QPS} (Database primary)100,000250,000 QPS100,000\text{--}250,000\text{ QPS} (In-memory node)
Database Load at 200k 🚨 200,000 QPS200,000\text{ QPS} \to Connection pool exhausted🛡️ 10,000 QPS10,000\text{ QPS} (95%95\% traffic absorbed in RAM)
Database Fleet SizingRequires 40–80 read replicas (Cost: >\30,000/\text{mo}$)Single primary + 2 read replicas (Cost: \approx \2,500/\text{mo}$)
Read Latency🚨 85.0 ms85.0\text{ ms} (Queuing backlog)1.8 ms1.8\text{ ms} (Ultra-stable )
WARNING

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., , Memcached, ) between the application tier and the primary persistence layer. Accessing RAM takes 100 ns\approx 100\text{ ns} compared to 1 ms10 ms\approx 1\text{ ms} - 10\text{ ms} for solid-state disk operations—a speedup of 10,000×10,000\times to 100,000×100,000\times.

Interactive Architecture Diagram
Synthesizing 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 (), 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 (<1 ms< 1\text{ ms}). 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 expires.
  • Trade-Offs:
    • Pros: Eliminates cache miss latency for hot keys.
    • Cons: Mispredicting access patterns causes unnecessary database load refreshing cold keys.
Interactive Architecture Diagram
Synthesizing 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 (HH):

Hit Ratio (H)=NhitsNhits+Nmisses\text{Hit Ratio } (H) = \frac{N_{\text{hits}}}{N_{\text{hits}} + N_{\text{misses}}}

Effective Access Time (EAT)=(H×Tcache)+((1H)×Tstorage)\text{Effective Access Time (EAT)} = (H \times T_{\text{cache}}) + ((1 - H) \times T_{\text{storage}})

Where TcacheT_{\text{cache}} is in-memory retrieval latency (0.5 ms0.5\text{ ms}) and TstorageT_{\text{storage}} is primary database query latency (15.0 ms15.0\text{ ms}).

NOTE

The 90% vs 99% Rule:

  • At H=90%H = 90\%: EAT=(0.90×0.5)+(0.10×15.0)=0.45+1.50=1.95 ms\text{EAT} = (0.90 \times 0.5) + (0.10 \times 15.0) = 0.45 + 1.50 = \mathbf{1.95\text{ ms}}.
  • At H=99%H = 99\%: EAT=(0.99×0.5)+(0.01×15.0)=0.495+0.15=0.645 ms\text{EAT} = (0.99 \times 0.5) + (0.01 \times 15.0) = 0.495 + 0.15 = \mathbf{0.645\text{ ms}} (3×3\times 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:

Trigger Early Refresh if: β×δ×ln(U)>Δ\text{Trigger Early Refresh if: } -\beta \times \delta \times \ln(U) > \Delta

Where:

  • Δ=TTLexpirationtcurrent\Delta = \text{TTL}_{\text{expiration}} - t_{\text{current}} (time remaining until expiration).
  • δ\delta is the observed execution time required to compute/fetch the item from the database.
  • β>0\beta > 0 is the aggressiveness multiplier (typically β=1.0\beta = 1.0).
  • UUniform(0,1)U \sim \text{Uniform}(0, 1) is a random floating-point value.
  • As Δ0\Delta \to 0, the probability of triggering an asynchronous background refresh approaches 1.01.0, 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 Diagram
Synthesizing vector architecture diagram...

In-Memory Node Layout

A production LRU node inside a doubly-linked list contains pointers and payload metadata:

Node FieldType / SizePurpose
prevuint64_t* (8 Bytes)Pointer to previous node toward Head
nextuint64_t* (8 Bytes)Pointer to next node toward Tail
keychar* (8 Bytes)Pointer to cached key string
valvoid* (8 Bytes)Pointer to serialized payload value
expires_atint64_t (8 Bytes)Unix timestamp for expiration
Total Overhead40 BytesPer-entry node overhead (excluding jemalloc chunk padding)

State-Transition Trace: LRU Doubly-Linked List Eviction

Under capacity limit C=3C = 3 entries, with initial state [C, B, A] (Head = C [MRU], Tail = A [LRU]):

Step #Event / InputIn-Memory / Distributed StateEvaluation & TransitionOutcome / Cache Latency
InitialBaseline stateCapacity C=3C = 3, Full
Map: {A, B, C}
Pointers: HEAD <-> [C] <-> [B] <-> [A] <-> TAIL
Head = C (MRU), Tail = A (LRU)
Cache full (3/33/3 slots allocated)
1GET B (Read Hit)Hash map lookup finds B pointer in O(1)O(1)Unlink B.prev & B.next; re-splice B to Head
Order becomes [B <-> C <-> A]; Tail A unchanged
Cache Hit (0.1 ms0.1\text{ ms})
B promoted to MRU; A remains LRU
2SET D = "val_d" (Write Insert)Key D absent; capacity ceiling reached (3/33/3)Evict Tail (A): delete map["A"], free node A
Allocate node D, splice as new Head
Eviction + Insert (0.2 ms0.2\text{ ms})
Order: [D <-> B <-> C]; C is now LRU Tail
3SET C = "v2" (Update Hit)Key C found at Tail positionUpdate value pointer C.val = "v2"; unlink from Tail
Splice C to Head; B becomes new Tail
Update + Promotion (0.1 ms0.1\text{ ms})
Order: [C <-> D <-> B]; B is now LRU Tail

Eviction Algorithm Comparison Matrix

Eviction AlgorithmUnderlying Data StructureTime ComplexityMemory per KeyVulnerability / Failure ModeReal-World Adoption
LRU (Least Recently Used)Doubly-Linked List + Hash MapO(1)O(1) get/put48 Bytes\approx 48\text{ Bytes}Scan Resistance Failure: A full table batch scan flushes entire working set. allkeys-lru, Memcached
LFU (Least Frequently Used)Frequency Buckets + Linked ListsO(1)O(1) get/put64 Bytes\approx 64\text{ Bytes}Historical Bias: Historical keys accumulate millions of hits and resist eviction long after going cold. allkeys-lfu
(First In, First Out)Queue / Circular Ring BufferO(1)O(1)16 Bytes\approx 16\text{ Bytes}Sub-optimal hit ratio; evicts hot items simply due to age.Varnish, Simple streaming buffers
Random EvictionReservoir / Uniform SamplingO(1)O(1)0 Bytes0\text{ Bytes}High variance; randomly evicts critical hot keys. allkeys-random
W-TinyLFUSLRU + Count-Min Sketch FilterO(1)O(1)8 Bytes\approx 8\text{ Bytes}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 Diagram
Synthesizing 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):

Write Sequence: Database.Update()    Cache.Delete(key)\text{Write Sequence: } \text{Database.Update}() \implies \text{Cache.Delete}(\text{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 ():

Interactive Architecture Diagram
Synthesizing vector architecture diagram...
  1. Transactional Guarantee: The application writes only to the database. The database writes changes atomically to its append-only transaction log ( / MySQL Binlog).
  2. Streaming Invalidation: A connector (e.g., Debezium) streams change events to .
  3. 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: distributes 16,384 slots across primary nodes. During dynamic scaling:
    1. Set target shard state: CLUSTER SETSLOT <slot> IMPORTING <source_node_id>.
    2. Set source shard state: CLUSTER SETSLOT <slot> MIGRATING <target_node_id>.
    3. Batch migrate keys atomically: MIGRATE <target_ip> <port> "" 0 5000 KEYS <key1> <key2>....
    4. Commit slot assignment: broadcast CLUSTER SETSLOT <slot> NODE <target_node_id> to all cluster nodes.
  • Client Redirection Handling (ASK vs. MOVED): In-flight keys on migrating slots return -ASK <slot> <target_ip:port>. The client issues an ASKING prefix 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 trigger CLUSTER FAILOVER TAKEOVER to elevate the replica gracefully and drain existing client TCP connections with zero data loss.

Part 2: Production Deep-Dive Locked1 Coin = 24 Hours

Unlock Complete Architecture & Production Runbooks

Your Balance:40 Coins

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.

Sections Included in This 24-Hour Pass:
4. Client-Side vs. Proxy-Mediated Routing Topologies
5. Real-World Distributed Engine Comparison
6. Critical Edge Cases & Distributed Failure Modes
7. Production Pitfalls & Anti-Patterns (The "Gotchas")
8. AWS Cloud Service Implementation & Production Patterns
9. Production Sizing Matrix & Operational Runbook
10. Production Diagnostics: Telemetry Signatures & Incident Response Playbook
Keeps page unlocked for exactly 24 hoursSpend coins to fund LLM & compute infrastructure