Caching Systems (Redis & Memcached)
The infrastructure behind caching — write strategies, data structures, and HA.
Redis and Memcached are in-memory data stores used to answer hot requests before they reach a slower database or service. A cache is not just a speed trick; it is a way to protect scarce backend capacity, absorb read spikes, and turn repeated expensive work into cheap lookups. The danger is that fast stale data can be worse than slow correct data.
The problem: hot reads and expensive recomputation
Databases are excellent sources of truth, but many user requests ask the same question repeatedly: "What is product 123?", "Is this feature flag enabled?", or "What are the top posts?" Without caching, every repeated request consumes database CPU, locks, network round trips, and query planner work.
request -> app -> cache GET product:123
├─ hit: return cached JSON in ~sub-ms
└─ miss: query database, compute response, SET cache with TTL- Latency: memory lookups are usually far faster than disk or complex database queries.
- Capacity: repeated reads hit the cache, leaving database capacity for writes and uncached queries.
- Failure isolation: a warm cache can keep read-only parts of a site alive during partial database trouble.
Redis vs Memcached
Memcached is a small, fast, distributed string cache. Redis is a richer data-structure server that can also act as a cache, lightweight queue, counter store, rate limiter, pub/sub broker, and coordination primitive.
| Dimension | Memcached | Redis |
|---|---|---|
| Core model | Key -> bytes/string | Key -> strings, hashes, lists, sets, sorted sets, streams, bitmaps |
| Best use | Simple ephemeral cache at very high throughput | Cache plus richer atomic data operations |
| Persistence | None; restart means cold cache | Optional RDB snapshots and AOF logs |
| Replication / HA | Usually client-side sharding; no built-in failover | Replicas, Sentinel, Cluster, managed HA |
| Memory behavior | Slab allocator, simple eviction | Configurable eviction policies and per-key TTLs |
| Operational complexity | Low | Higher because Redis can become critical state |
Redis data structures and real use cases
Redis is popular because operations run close to the data and are atomic on a single instance or shard. Instead of fetching a blob, changing it in app code, and writing it back, you ask Redis to perform a data-structure operation directly.
| Structure | Commands | Use cases |
|---|---|---|
| String | GET, SET, INCR | Cached HTML/JSON, counters, rate-limit tokens |
| Hash | HGET, HSET | Object fields such as user session attributes |
| List | LPUSH, BRPOP | Simple work queues, recent activity lists |
| Set | SADD, SISMEMBER | Membership checks, unique viewers, feature cohorts |
| Sorted set | ZADD, ZRANGE | Leaderboards, priority queues, expiring holds |
| Stream | XADD, XREADGROUP | Durable-ish event streams and consumer groups |
ZADD leaderboard 8700 user:42
ZADD leaderboard 9100 user:99
ZREVRANGE leaderboard 0 9 WITHSCORES
# Redis maintains score order, so top-N does not require scanning every player.Atomic operations matter
INCR lets many clients update a counter without lost updates. Sorted sets keep rankings ordered while scores change. Lua scripts or transactions can combine a few operations when a rate limiter or seat hold must be checked and updated as one unit.
Persistence, replication, Sentinel, and Cluster
A pure cache can disappear and be rebuilt. Redis often holds state that is expensive or temporarily important, so teams enable persistence and high availability. Be clear whether Redis is disposable cache or semi-durable operational state.
| Feature | How it works | Trade-off |
|---|---|---|
| RDB snapshots | Periodic point-in-time dump to disk | Compact and fast to restart, but can lose changes since last snapshot |
| AOF | Append every write command to a log | Better durability, more disk I/O and rewrite management |
| Replication | Replica copies primary asynchronously | Read scale and failover target, but replicas can lag |
| Sentinel | Monitors primary and promotes replica | HA for non-cluster Redis; clients must follow new primary |
| Cluster | Shards keyspace across masters with replicas | Scales memory/write load, but multi-key operations need same hash slot |
Write strategies: cache-aside, write-through, write-back
The write strategy defines how the cache and source of truth stay aligned. Most bugs come from forgetting that an update must either invalidate or refresh every cached representation affected by the change.
| Strategy | Read path | Write path | Best for | Risk |
|---|---|---|---|---|
| Cache-aside | App reads cache; on miss reads DB and fills cache | Write DB, then delete or update cache | Default web-app pattern | Stale data if invalidation is missed |
| Write-through | Cache should already contain fresh value | Write cache and DB together | Small objects needing fresh reads | Higher write latency and coupling |
| Write-back | Reads cache | Write cache first, flush DB later | Very high write bursts where loss is acceptable | Data loss if cache dies before flush |
| Refresh-ahead | Cache refreshes before expiry | Background job recomputes hot keys | Expensive but predictable data | Wasted work for keys that cool down |
def get_product(id):
key = f"product:{id}"
cached = redis.get(key)
if cached:
return decode(cached)
product = db.query("SELECT * FROM products WHERE id = ?", id)
redis.set(key, encode(product), ex=300) # 5 minute TTL
return product
def update_product(id, patch):
db.update_product(id, patch) # source of truth first
redis.delete(f"product:{id}") # force next read to refillEviction policies, stampedes, and hot keys
Cache memory is finite. When Redis or Memcached is full, it must evict something or reject writes. Good cache design treats eviction as normal, not exceptional: the database must still answer correctly when the cache is empty.
- TTL: each key expires after a configured time. Add jitter so thousands of keys do not expire at the same second.
- LRU / LFU: evict least-recently-used or least-frequently-used keys when memory is full.
- noeviction: reject new writes instead of evicting. Useful when keys are not safely disposable.
val = redis.get(key)
if val is not None:
return val
# only one request should rebuild the hot key
if redis.set("lock:" + key, "1", nx=True, ex=10):
val = recompute_from_db()
redis.set(key, val, ex=300 + random_jitter())
redis.delete("lock:" + key)
return val
sleep_briefly()
return redis.get(key) or fallback_response()- Memcached is a simple disposable string cache; Redis is a richer data-structure server with persistence, replication, and clustering options.
- Redis structures such as hashes, sets, sorted sets, counters, and streams let you model rate limits, leaderboards, presence, queues, and seat holds atomically.
- RDB snapshots and AOF logs improve Redis recovery, but configuration determines the real data-loss window.
- Cache-aside is the default write strategy; every write must refresh or invalidate affected keys, and the database must remain the source of truth unless explicitly designed otherwise.
- Eviction, stampedes, and hot keys are normal production problems; use TTL jitter, single-flight, replication, local caches, and careful eviction policies.
Mark it complete to track your progress through the workbook.