Chapter 22: Distributed Caches

Redis and Memcached power the world's fastest web services. Understanding their internals, clustering strategies, and operational patterns is critical for system design interviews and production systems.

Redis Architecture

Single-Threaded Event Loop

Component Design Why Fast Tradeoffs
Event Loop Single-threaded, epoll/kqueue No locks, no context switching, CPU cache friendly Cannot use multiple cores for single command
I/O Multiplexing Async I/O, non-blocking sockets Handles 10k+ concurrent connections Blocking commands (BLPOP) hold thread
In-Memory All data in RAM O(1) access, no disk seeks Limited by RAM size, volatile
Data Structures Optimized C implementations ziplist, intset, skiplist optimizations Memory overhead per key (~90 bytes)
Pipelining Batch commands, reduce RTT 10x throughput improvement No atomicity across pipeline
Background Threads Lazy free, AOF fsync (Redis 4.0+) Non-blocking DEL for large keys Main thread still single

Redis Data Structures

Type Encoding Operations Use Cases Time Complexity
String Raw, int, embstr GET, SET, INCR, APPEND Session cache, counters, rate limiting O(1)
Hash ziplist (small), hashtable HGET, HSET, HINCRBY, HGETALL User profiles, object caching O(1) per field, O(N) HGETALL
List ziplist (small), linkedlist, quicklist LPUSH, RPUSH, LPOP, LRANGE Message queues, timelines, activity feeds O(1) push/pop, O(N) LRANGE
Set intset (small ints), hashtable SADD, SREM, SISMEMBER, SINTER Tags, unique visitors, social graphs O(1) add/remove/check, O(N) SINTER
Sorted Set ziplist (small), skiplist + hashtable ZADD, ZRANGE, ZRANK, ZINCRBY Leaderboards, priority queues, time series O(log N) add, O(log N + M) range
Stream radix tree XADD, XREAD, XREADGROUP Event sourcing, message broker, logs O(1) append, O(N) read N entries
HyperLogLog Probabilistic, 12KB PFADD, PFCOUNT, PFMERGE Unique counts (0.81% error), cardinality O(1) add/count
Bitmap String with bit ops SETBIT, GETBIT, BITCOUNT, BITOP User online status, feature flags, analytics O(1) get/set, O(N) BITCOUNT
Geospatial Sorted set (geohash) GEOADD, GEORADIUS, GEODIST Location tracking, nearby search O(log N) add, O(N+log M) radius
Interview Tip: When asked "Why is Redis fast?", mention: (1) in-memory, (2) single-threaded = no locks, (3) optimized data structures, (4) event-driven I/O. Don't just say "it's in-memory" — that's incomplete.
graph TD
    A[Client Requests] --> B[I/O Multiplexer
epoll/kqueue] B --> C[Event Loop
Single Thread] C --> D{Command Type} D -->|Read| E[In-Memory Data Structures] D -->|Write| E D -->|Blocking| F[Blocking Queue] E --> G[Response Buffer] F --> G C --> H[Background Threads
Lazy Free, AOF] G --> I[Send Response] style C fill:#ff6b6b,color:#1a1a2e style E fill:#4ecdc4,color:#1a1a2e style H fill:#ffe66d

Redis Persistence

RDB (Redis Database Snapshot)

Aspect Details
How It Works fork() process → child writes snapshot → rename dump.rdb atomically
Copy-on-Write Parent continues serving, child has frozen memory view. Only modified pages duplicated.
Triggers SAVE (blocking), BGSAVE (background), auto (save 900 1, save 300 10, save 60 10000)
Format Binary, compressed, single point-in-time snapshot
Pros Compact, fast restart, minimal performance impact, good for backups
Cons Data loss (up to last snapshot interval), fork() can double memory usage spike, slow on large datasets

AOF (Append-Only File)

Aspect Details
How It Works Log every write command → replay on restart
Sync Policies always (fsync every write, slow), everysec (default, 1s data loss), no (OS decides, fast)
Rewrite Background process compacts AOF (BGREWRITEAOF), replaces multi-step with SET
Format Text (RESP protocol), human-readable, rewritable
Pros Minimal data loss (1s with everysec), auto-rewrite, append-only (crash safe)
Cons Larger files, slower restart, write amplification

Hybrid Persistence (Redis 4.0+)

Aspect Details
How It Works AOF rewrite uses RDB format for base + AOF for incremental changes
Benefits Fast restart (RDB) + minimal data loss (AOF tail). Best of both.
Config aof-use-rdb-preamble yes

Persistence Comparison

Feature RDB AOF (everysec) Hybrid None
Data Loss Minutes ~1 second ~1 second All
Restart Speed Fast Slow (large AOF) Fast Instant
File Size Small (compressed) Large (grows over time) Medium None
Write Performance High (periodic) Medium (fsync overhead) Medium Highest
Memory Spike 2x (fork COW) None (append) 2x (rewrite) None
Corruption Risk Low (atomic rename) Medium (truncate last command) Low None
Use Case Backups, tolerate data loss Critical data, durability Production default Pure cache
Interview Scenario: "Your Redis server crashes. What determines data loss?" Answer: Persistence mode. RDB loses data since last snapshot (minutes). AOF everysec loses ~1 second. Hybrid is same as AOF. No persistence loses everything. Explain fork() COW for RDB.

Redis Cluster

Architecture Overview

Component Details
Hash Slots 16384 slots (0-16383). CRC16(key) % 16384 determines slot. Allows easy resharding.
Topology Minimum 3 master nodes. Each master owns subset of slots. Replicas for HA.
Gossip Protocol Every node talks to every node (TCP). Heartbeat, slot mapping, failure detection.
Decentralized No proxy, no central coordinator. Client-side routing. Nodes detect failures via voting.
Resharding Live, non-blocking. Move slots between nodes. Clients auto-redirect via MOVED.
Multi-Key Ops Only work if keys in same slot. Use hash tags: {user123}:profile, {user123}:sessions.

Client Redirection

Response Meaning Client Action When
MOVED Slot permanently moved to another node Update slot map, redirect to new node After resharding, stale client cache
ASK Slot temporarily being migrated Send ASKING + retry once (no cache update) During live resharding, key migrated but slot not yet
CLUSTERDOWN Cluster cannot serve (quorum lost) Return error to user, retry or failover Majority of masters down
TRYAGAIN Slot data in flux Retry after backoff Resharding race condition

Replica Promotion

Step Action Time
1. Failure Detection Master misses heartbeat (NODE_TIMEOUT, default 15s). Marked PFAIL by nodes. 15s
2. Failure Propagation Majority of masters mark it PFAIL → FAIL. Gossip spreads FAIL state. +2s
3. Replica Election Replicas request votes. Most up-to-date replica (replication offset) wins. +1s
4. Promotion Winning replica becomes master, broadcasts slot ownership via gossip. +1s
5. Convergence Cluster updates slot map. Clients receive MOVED, update cache. +2s
Total Downtime 15-25 seconds typical (tunable via NODE_TIMEOUT) ~20s
graph TB
    subgraph "Redis Cluster - 16384 Slots"
        M1[Master 1
Slots 0-5460] M2[Master 2
Slots 5461-10922] M3[Master 3
Slots 10923-16383] R1[Replica 1A] R2[Replica 1B] R3[Replica 2A] R4[Replica 3A] M1 -.->|replicates| R1 M1 -.->|replicates| R2 M2 -.->|replicates| R3 M3 -.->|replicates| R4 M1 <-->|gossip| M2 M2 <-->|gossip| M3 M3 <-->|gossip| M1 end C[Client] -->|GET user:123| M2 M2 -->|MOVED 7890
master3:6379| C C -->|GET user:123| M3 M3 -->|"value"| C style M1 fill:#ff6b6b,color:#1a1a2e style M2 fill:#4ecdc4,color:#1a1a2e style M3 fill:#ffe66d style R1 fill:#ffcccc style R2 fill:#ffcccc style R3 fill:#ccf5f2 style R4 fill:#fff4cc
Common Mistake: "Redis Cluster has 16 shards." Wrong. It has 16384 hash slots. You choose number of masters (typically 3-10). More slots than nodes allows fine-grained resharding without rebalancing all data.

Redis Sentinel

Sentinel vs Cluster

Feature Sentinel Cluster
Purpose HA for single master-replica setup Sharding + HA for horizontal scaling
Topology 1 master + N replicas (no sharding) N masters (sharded) + replicas
Data Distribution All data on master, replicated to all replicas Data partitioned across masters by hash slot
Failover Sentinel processes monitor & promote replica Built-in, replicas self-elect via gossip
Client Connects to Sentinel, gets current master address Smart client with slot map, direct to node
Scalability Vertical only (single master bottleneck) Horizontal (add masters for more capacity)
Complexity Simple, 3 Sentinel processes + Redis nodes Complex, minimum 6 nodes (3 master + 3 replica)
Use Case Small dataset, need HA, avoid cluster overhead Large dataset, need sharding + HA
Quorum Sentinel quorum (e.g., 2/3 Sentinels agree) Master quorum (majority of masters)
Split Brain Possible if network partition + write to old master Less risk, majority-based slot ownership

Sentinel Operations

Operation Details
Monitoring Ping Redis instances every 1s. If master down >30s (configurable), mark SDOWN (subjectively down).
Quorum If quorum Sentinels mark SDOWN → ODOWN (objectively down). Trigger failover.
Failover Steps 1) Elect leader Sentinel. 2) Select best replica (priority, offset, run_id). 3) Promote replica to master. 4) Reconfigure other replicas. 5) Notify clients.
Configuration sentinel.conf: sentinel monitor mymaster 127.0.0.1 6379 2 (2=quorum)
Client Discovery Client connects to Sentinel, asks "who is master?" (SENTINEL get-master-addr-by-name), caches address.
When to use what: Dataset < memory of single node + need HA → Sentinel. Dataset > single node OR need >100k ops/s → Cluster. Just need cache with no HA → single Redis instance.

Memcached

Architecture

Component Design Implications
Threading Multi-threaded (libevent + worker threads) Scales across cores, higher throughput than Redis on multi-core. Lock contention on hot keys.
Data Model Simple key-value (opaque bytes) No rich data structures. App must serialize/deserialize. No atomic ops beyond CAS.
Memory Slab allocator (fixed-size chunks) Reduces fragmentation, but wasted space if object doesn't fit slab perfectly. No memory reclaim between slabs.
Eviction LRU per slab class Old items evicted when slab full. Global memory limit (-m 64).
Persistence None (pure cache) Restart = empty cache. Cold start storm risk.
Replication None (client-side duplication if needed) Node failure = lost data. Client retries → DB.
Protocol Text (ASCII) or binary Simple, easy to debug. Binary is faster.

Slab Allocation

Concept Details
Slab Classes Predefined size classes: 96B, 120B, 152B, ..., 1MB. Growth factor (default 1.25).
Chunk Fixed-size block within slab. Object stored in smallest fitting chunk.
Slab Page 1MB page divided into chunks. Allocated to slab class on first use.
Internal Fragmentation 90-byte object in 96-byte chunk wastes 6 bytes. 150-byte object in 152-byte chunk wastes 2 bytes.
Slab Imbalance If workload changes, pages stuck in wrong slab class. Use -o slab_reassign,slab_automove (1.4.11+).
Tuning Adjust growth factor (-f 1.15) for finer granularity, less waste.

Client-Side Consistent Hashing

Aspect Details
Algorithm Hash key → ring (0-2^32). Find next node clockwise. Ketama, jump hash, rendezvous hash.
Virtual Nodes Each physical node gets 150-200 virtual nodes on ring. Improves balance when nodes added/removed.
Node Failure Keys redistribute to next node. ~1/N keys remapped (N=nodes). High cache miss rate spike.
Adding Node ~1/(N+1) keys remapped. Gradual: warm up new node, then add to pool.
Pros Decentralized, no SPOF, minimal remapping, client controls topology.
Cons All clients must have same server list. No rebalancing (empty cache after scale).
graph TB
    C["Client Library Consistent Hash"]

    R --> N1["Node 1 V1,V2,V3"]
    R --> N2["Node 2 V4,V5,V6"]
    R --> N3["Node 3 V7,V8,V9"]

    N1 --> S1["Slab Allocator 96B, 120B, ..., 1MB"]
    N2 --> S2[Slab Allocator]
    N3 --> S3[Slab Allocator]

    S1 --> M1["Memory LRU per slab"]
    S2 --> M2[Memory]
    S3 --> M3[Memory]

    style C fill:#ff6b6b,color:#1a1a2e
    style R fill:#4ecdc4,color:#1a1a2e
    style N1 fill:#ffe66d
    style N2 fill:#ffe66d
    style N3 fill:#ffe66d
Memcached CAS: Compare-And-Set prevents lost updates. Client: GET key (returns value + cas_token) → modify locally → SET key value cas_token. If token stale (another client modified), SET fails. Retry. Used for counters, inventory.

Redis vs Memcached

Feature Redis Memcached Winner
Threading Single-threaded (6.0+ I/O threads) Multi-threaded Memcached (scales cores)
Data Structures String, Hash, List, Set, SortedSet, Stream, HLL, Bitmap, Geo Key-value (opaque bytes) Redis (rich ops)
Persistence RDB, AOF, hybrid None Redis
Replication Built-in master-replica None (client-side) Redis
Clustering Redis Cluster (built-in) Client-side consistent hashing Redis (easier)
Memory Efficiency 90 bytes overhead per key ~56 bytes overhead Memcached (lower overhead)
Eviction 8 policies (LRU, LFU, volatile, allkeys, ttl) LRU per slab Redis (flexible)
Max Value Size 512 MB (configurable) 1 MB (default, tunable) Redis
Atomic Ops INCR, DECR, HINCRBY, ZADD, List ops, Lua scripts, transactions INCR, DECR, CAS Redis (more ops)
Pub/Sub Yes (channels, patterns, sharded - 7.0) No Redis
Lua Scripting Yes (server-side logic) No Redis
Geospatial GEOADD, GEORADIUS No Redis
Transactions MULTI/EXEC, WATCH No (only CAS) Redis
Memory Fragmentation Can occur (jemalloc), use MEMORY PURGE Lower (slab allocator) Memcached
Throughput (simple GET) ~100k ops/s (single core) ~300k ops/s (multi-core) Memcached (multi-threaded)
Use Case Rich data ops, persistence, queues, leaderboards, sessions, counters Pure cache, simple KV, horizontal scale across many nodes Depends

Decision Matrix

Requirement Choose Redis Choose Memcached
Need persistence ✓
Need replication/HA ✓ (Sentinel/Cluster) Roll your own
Need lists, sets, sorted sets ✓ Serialize in app
Need atomic counters, INCR ✓ (simpler) CAS loop
Need pub/sub, queues ✓ No
Pure cache, no persistence OK (disable) ✓ (simpler)
Need max throughput (multi-core) Redis 6.0+ I/O threads ✓ (native)
Many small objects (<1KB) Higher overhead ✓ (lower overhead)
Large values (>1MB) ✓ Tune or split
Existing Memcached expertise Migration cost ✓
Default Answer: "Use Redis unless you have specific reason for Memcached." Redis superset of Memcached features. Only use Memcached if: (1) already deployed at scale, (2) pure cache with no persistence, (3) need absolute max throughput on many cores.

Hot Key Problem

Problem Description

Aspect Details
Symptom Single Redis node overwhelmed. 100k+ QPS on one key. CPU maxed, latency spikes, connection timeouts.
Causes Viral content (celebrity tweet), flash sale item, shared config key, global counter.
Cluster Impact Hot key on one shard → that node bottleneck → uneven load. Other nodes idle.
Single-Threaded Bottleneck Redis single-threaded → can't distribute hot key load across cores on same machine.

Solutions

Solution How It Works Pros Cons
1. Local Cache App server caches hot key in-process (Guava, Caffeine). TTL 1-10s. Reduces Redis QPS 90%+. Simple. Low latency. Stale data (up to TTL). Invalidation hard. Memory per app server.
2. Key Splitting Replicate hot key: hot_key:0, hot_key:1, ..., hot_key:N. Random read. Distributes reads across cluster nodes. Write amplification (update all copies). Stale on updates. Adds complexity.
3. Read Replicas Redirect reads to replicas (READONLY in Cluster). Scale reads horizontally. No code change (client config). Consistent. Replication lag. More nodes. Writes still bottleneck.
4. CDN/Edge Cache Cache response at CDN (Cloudflare, Fastly). For read-only data. Offloads origin completely. Global distribution. Only for HTTP GET. Stale data. TTL management.
5. Dedicated Node Move hot key to separate Redis instance (not in cluster). Route via key prefix. Isolates blast radius. Can use bigger machine. Manual sharding. Operational overhead.
6. Lua Script + Replica Lua script reads from random replica node using redis.call(). Dynamic load balancing. Complex. Replication lag.

Hot Key Detection

Method Details
Redis --hotkeys redis-cli --hotkeys (requires maxmemory-policy allkeys-lfu or volatile-lfu). LFU tracks access frequency.
MONITOR redis-cli MONITOR | head -10000 → count keys. Heavy overhead, use sparingly.
Client-Side Logging Log keys with latency >10ms. Aggregate in Splunk/ELK. Track QPS per key.
Proxy Metrics Twemproxy, Envoy, or custom proxy tracks key frequency.
Redis Exporter Prometheus redis_exporter. Monitor CPU per node. Spike = hot key on that shard.
Follow-up: "Celebrity posts a tweet, 10M users hit your cache for same key. What do you do?" Answer: (1) Local cache (10s TTL) in app servers → 99% reduction. (2) If still overloaded, replicate key 10x, random read. (3) If writes high, queue writes + batch update. (4) Pre-populate cache before tweet goes live (if scheduled).

Cluster Patterns

Pattern Architecture Pros Cons Use Case
Client-Side Sharding Client library does consistent hashing. Direct to node. No proxy overhead. Simple. Minimal latency. All clients need same server list. No auto-rebalance. Node failure = cache miss storm. Memcached, small Redis pools
Proxy (Twemproxy) Proxy layer (Twemproxy, Envoy) does sharding. Client → Proxy → Redis. Centralized routing. Clients unaware of topology. Connection pooling. Metrics. Extra hop (+1ms). Proxy SPOF (mitigate with multiple). No auto-failover (needs Sentinel). Legacy apps, mixed Redis/Memcached, need metrics
Redis Cluster Decentralized. Client has slot map. Direct to shard. MOVED redirect. No proxy. Auto-rebalance. Built-in HA. Scales to 1000 nodes. Complex setup. Multi-key ops limited. Resharding causes brief latency spike. Large scale (>100GB data), need auto-sharding + HA
Sentinel + Client Sharding Multiple Redis master-replica pairs. Sentinel per pair. Client shards across masters. HA per shard. Simpler than Cluster. Flexible. Manual sharding. More Sentinel processes. Resharding requires downtime or dual-write. Medium scale, need HA, avoid Cluster complexity
Codis Proxy-based (Go). Centralized config (ZooKeeper). Auto-rebalance slots. Redis Cluster features without Redis Cluster. Dashboard. Supports Redis 2.8/3.0. Extra component. Less adoption than Redis Cluster. Proxy overhead. China companies (JD.com, Wandoujia)
Managed (AWS ElastiCache) Cloud provider handles clustering, failover, backups. Zero ops. Auto-scaling. Monitoring. Multi-AZ. Vendor lock-in. Cost. Less control. Version lag. Teams without Redis expertise, fast iteration
graph TB

        C2[Client] --> P["Twemproxy lt br/>Proxy"]

        C3["Client lt br/>Slot Map"] -->|slot 1234| R7["Master 1 lt br/>Slots 0-5460"]
        C3 -->|MOVED| R8["Master 2 lt br/>Slots 5461-10922"]
        R8 -->|slot 7890| C3

    style P fill:#ff6b6b,color:#1a1a2e
    style C3 fill:#4ecdc4,color:#1a1a2e
    style R7 fill:#ffe66d
    style R8 fill:#ffe66d
Comparison Summary: Small scale (1-3 nodes) → client-side. Medium (3-10 nodes, need HA) → Sentinel + client sharding. Large (>10 nodes, auto-scale) → Redis Cluster. Legacy/polyglot → Twemproxy. No Redis expertise → managed.

Facebook's Memcache at Scale

Architecture Overview

Component Details
Scale Trillions of items, billions of requests/sec, thousands of servers across multiple regions.
Cluster Frontend cluster (web servers + Memcached). Consistent hashing. ~1000 nodes per cluster.
Regions Primary region (writes). Secondary regions (read replicas). MySQL replicates cross-region.
Invalidation Web server writes DB → DB sends invalidate message via McSqueal/McRouter → invalidate cache across region.

Key Optimizations

Optimization Problem Solved How It Works
Leases Thundering herd: 1000 requests for expired key → 1000 DB queries First miss gets lease token. Sets cache with lease. Others wait or serve stale. Only 1 DB query.
Stale Sets Cache deleted, multiple requests try to set → race → wrong version cached Lease token includes version. Stale set rejected.
Regional Pools Hot keys overwhelm single cluster Wildcard keys (e.g., celebrity profiles) replicated to separate pool. Load balanced.
Gutter Servers Memcache node failure → all requests to DB → DB overload Failed node requests routed to "gutter" pool (backup cache). Temporary. Lower hit rate OK.
Batching 1000 small requests = high latency Client batches multi-get. Single round-trip fetches 100+ keys.
UDP for GETs TCP overhead for small reads UDP for GET (connectionless, lower latency). TCP for SET (reliability).
McRouter Client-side logic duplicated in every language Local proxy handles routing, pooling, failover. Embeddable library or standalone.

Leases Deep Dive

Step Action Why
1. Cache Miss GET returns miss + lease token (64-bit number, TTL 10s) Token proves "you asked first"
2. Query DB First client queries DB Only 1 client does work
3. Concurrent Requests Other clients GET → return miss, no lease (lease already granted) Prevents thundering herd
4. Wait or Stale Subsequent clients wait 100ms and retry, or serve stale if available Balance freshness vs load
5. Set with Lease First client: SET key value lease_token Verifies token valid (not invalidated)
6. Invalidation If DB updated before SET, invalidation message revokes lease → SET rejected Prevents stale set
graph TB
    subgraph "Facebook Memcache Architecture"
        WS[Web Server
PHP + McRouter] WS -->|GET key| MC[Memcache
Frontend Cluster] MC -->|miss + lease| WS WS -->|query| DB[(MySQL)] DB -->|result| WS WS -->|SET key lease| MC DB -->|invalidate| MCS[McSqueal
Invalidation] MCS -->|delete key| MC MCS -->|delete key| MC2[Memcache
Other Clusters] MC -->|failure| G[Gutter Pool
Backup Cache] WS -.->|fallback| G MC --> RP[Regional Pool
Hot Keys] end style MC fill:#4ecdc4,color:#1a1a2e style DB fill:#ff6b6b,color:#1a1a2e style G fill:#ffe66d style RP fill:#95e1d3,color:#1a1a2e
Key Insight: Leases solve two problems: (1) thundering herd (only 1 DB query on miss), (2) stale sets (invalidation during fetch → lease revoked → set rejected). This pattern is reusable beyond Memcache.

Cache-DB Consistency

Write Patterns

Pattern Steps Pros Cons Consistency
Cache-Aside
(Lazy Load)
Read: Check cache → miss → query DB → set cache. Write: Update DB → delete cache. Simple. Cache only hot data. Fault-tolerant (cache down → still works). Cache miss penalty. Stale data on write (until next read). Race conditions. Eventually consistent
Read-Through Cache library auto-loads from DB on miss. App only talks to cache. Transparent. Consistent API. Built-in loader. Cache library must support. Tight coupling. Single point of failure. Eventually consistent
Write-Through Write: Update cache → cache updates DB synchronously. Read: Check cache (always hit). Always consistent. Simple reads. Slower writes (sync). Wasted cache space (cold data cached). Strongly consistent
Write-Behind
(Write-Back)
Write: Update cache → return. Async worker flushes to DB (batched). Fast writes. Batch DB updates. Absorbs spikes. Risk of data loss (cache crash before flush). Complex (need queue). Eventual consistency. Eventually consistent
Refresh-Ahead Background job refreshes cache before TTL expires. No miss penalty. Predictable latency. Complex. Wasted work if key not accessed. Stale if DB updated. Eventually consistent

Cache Invalidation Strategies

Strategy How When to Use Pitfalls
Delete on Write Update DB → delete cache key Most common. Cache-aside pattern. Race: T1 updates DB, T2 reads stale cache before T1 deletes. Solution: delete after commit.
Set on Write Update DB → update cache High read/write ratio. Avoid miss penalty. Race: T1 updates DB (v1), T2 updates DB (v2), T2 sets cache, T1 sets cache → stale v1 cached.
TTL Expiration Set TTL (e.g., 5 min). Stale OK until expire. Acceptable staleness. Reduce invalidation complexity. Unbounded staleness if never accessed. Not suitable for critical data.
Event-Driven DB change → pub event (Kafka) → consumer invalidates cache Microservices. Multiple cache layers. Cross-region. Complexity. Event ordering. Exactly-once hard.
CDC (Change Data Capture) DB binlog → stream processor → invalidate cache DB writes outside your app (admin tools, batch jobs). Guaranteed. Binlog lag. Schema coupling. Tooling (Debezium, Maxwell).

Race Condition: Delete-Then-Set

Time Thread 1 (Write) Thread 2 (Read) Cache State DB State
t0 key=v1 key=v1
t1 Update DB: key=v2 key=v1 key=v2
t2 Cache miss (T1 deleted) empty key=v2
t3 Query DB: get v2 empty key=v2
t4 Delete cache empty key=v2
t5 Set cache: key=v2 key=v2 ✓ key=v2

Race Condition: Set-Then-Set

Time Thread 1 Thread 2 Cache State DB State
t0 key=v1 key=v1
t1 Update DB: key=v2 key=v1 key=v2
t2 Update DB: key=v3 key=v1 key=v3
t3 Set cache: key=v3 key=v3 key=v3
t4 Set cache: key=v2 key=v2 ✗ stale! key=v3

Lease-Get Pattern (Facebook)

Step Action Benefit
1. Read GET key → miss + lease token Token proves "I'm computing value"
2. Compute Query DB, compute expensive result Only 1 thread does work (lease holder)
3. Write Update DB (if write) → invalidate → lease revoked Prevents stale set
4. Set SET key value lease_token Rejected if lease invalidated
Safe Pattern: Delete on write (not set). Delete AFTER DB commit (in transaction callback). For reads: check cache → miss → get lease → query DB → set with lease. Lease prevents stale set race. For < 1% inconsistency tolerance: use TTL + delete.

Operations

Memory Fragmentation

Aspect Details
Cause Frequent key updates with size changes. Allocator (jemalloc) can't reuse freed memory blocks perfectly.
Detection INFO memory → mem_fragmentation_ratio = used_memory_rss / used_memory. >1.5 is high.
Redis Fix (4.0+) CONFIG SET activedefrag yes. Background defrag. CPU cost.
Restart Restart Redis (loads RDB clean). Downtime or failover to replica.
Prevention Avoid frequent updates to variable-size keys. Use fixed-size hashes. Use append-only structures (List, Stream).

Maxmemory Policies

Policy Behavior Use Case
noeviction Return errors on write when max memory reached Critical data, cannot lose anything. Monitor and scale before full.
allkeys-lru Evict least recently used keys (any key) Pure cache, all keys eligible, standard LRU cache behavior
allkeys-lfu Evict least frequently used keys (any key) Workload with hot keys accessed repeatedly, cold keys accessed once
allkeys-random Evict random keys All keys equally important, fast eviction
volatile-lru Evict LRU among keys with TTL set Mix of cache (with TTL) and persistent data (no TTL). Evict only cache.
volatile-lfu Evict LFU among keys with TTL Same as volatile-lru but frequency-based
volatile-random Evict random key with TTL Fast eviction, only volatile keys
volatile-ttl Evict key with shortest TTL Explicit priority: short TTL = less important

Pipelining vs Transactions

Feature Pipelining Transactions (MULTI/EXEC)
Purpose Reduce RTT, batch commands Atomic execution, all-or-nothing
Atomicity None (commands independent) All commands execute atomically (no interleave)
Network Send all commands at once, read all responses Send MULTI, commands, EXEC. Single round-trip.
Rollback No No (Redis doesn't rollback on error, just skips failed command)
Isolation No (other clients can interleave) Isolated (no commands from other clients between MULTI/EXEC)
Use Case 100 independent GETs → 1 RTT instead of 100 DECR inventory, check >=0, SET status. Must be atomic.

Lua Scripting

Aspect Details
Why Complex logic server-side. Reduce network RTT. Atomic execution. No race conditions.
Execution Runs atomically (blocks other commands). Single-threaded. No concurrent access.
Commands EVAL script numkeys key1 key2 arg1 arg2, EVALSHA sha1 ..., SCRIPT LOAD script
Example Rate limiter: local count = redis.call('INCR', KEYS[1]); if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end; return count
Pitfalls Blocks Redis during execution. Keep scripts fast (<10ms). No infinite loops. No random/time (non-deterministic → replication breaks).
Use Cases Rate limiting, distributed lock, leaderboard updates, conditional updates, deduplication

Monitoring Metrics

Metric Command What to Watch
Memory Usage INFO memory → used_memory_human >80% of maxmemory → scale up or add nodes
Fragmentation mem_fragmentation_ratio >1.5 → defrag or restart
Evictions INFO stats → evicted_keys Rising → out of memory, increase maxmemory
Hit Rate keyspace_hits / (keyspace_hits + keyspace_misses) <80% → bad cache, wrong TTL, or cold start
Latency redis-cli --latency, SLOWLOG GET >1ms → investigate slow commands, network, or CPU
Replication Lag INFO replication → master_repl_offset - slave_repl_offset >1MB or >1s → replica slow, network issue, or master overload
Connections INFO clients → connected_clients Near maxclients → connection leak or need pooling
CPU INFO cpu → used_cpu_sys, used_cpu_user High → slow commands, Lua scripts, or hot keys
Slow Commands SLOWLOG GET 10 KEYS, SMEMBERS on large sets, HGETALL on large hashes
Blocked Clients INFO stats → blocked_clients Using BLPOP/BRPOP. Spike → queue backlog.
Red Flags: (1) Fragmentation >1.5 → restart or defrag. (2) Evictions >0 with noeviction policy → out of memory. (3) Hit rate <80% → wrong data cached or TTL too short. (4) Replication lag >10s → replica will be dropped. (5) Slow commands (KEYS, SMEMBERS) in SLOWLOG → use SCAN, SSCAN.

Summary

Topic Key Takeaway
Redis Architecture Single-threaded event loop, in-memory, optimized data structures. Fast = no locks + CPU cache friendly.
Redis Persistence RDB (snapshot, fast restart). AOF (append-only, durable). Hybrid (best of both). Production: hybrid.
Redis Cluster 16384 hash slots, decentralized, gossip protocol. MOVED for permanent, ASK for temporary. 20s failover.
Redis Sentinel HA for single master-replica. No sharding. Use when data fits 1 node. Cluster = sharding + HA.
Memcached Multi-threaded, slab allocator, no persistence, client-side hashing. Use for pure cache at massive scale.
Redis vs Memcached Redis = richer, persistent, easier clustering. Memcached = simpler, faster on multi-core, lower overhead.
Hot Key Problem Single key overwhelms node. Solutions: local cache (best), key splitting, read replicas, CDN.
Cluster Patterns Client-side (simple, no HA). Proxy (centralized, metrics). Redis Cluster (auto-sharding, HA). Managed (zero ops).
Facebook Memcache Leases solve thundering herd + stale set. Gutter servers for failure. Regional pools for hot keys.
Cache-DB Consistency Delete on write (safe). Set on write (race risk). Lease-get pattern prevents stale set race.
Operations Monitor: fragmentation, evictions, hit rate, replication lag. Use pipelining for throughput. Lua for atomicity.