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. |