Redis & Caching
You added Redis caching to cut latency by 40%. Expect questions on cache patterns, eviction, persistence, and pitfalls.
Redis Architecture
Redis is an in-memory data structure store. It is primarily single-threaded for command execution. This means commands are executed sequentially, guaranteeing atomicity and eliminating the need for internal locks. (Note: Redis 6+ introduced I/O threads for network parsing, but command execution remains single-threaded).
Data Structures
Redis is not just a key-value store; it's a data structure server.
| Type | Commands | Use case |
|---|---|---|
| String | GET, SET, INCR | Cached JSON blobs, counters, session tokens, rate limits. (Max 512MB) |
| Hash | HGET, HSET, HGETALL | Storing object fields. Fetching specific user properties without parsing full JSON. |
| List | LPUSH, RPOP, LTRIM | Message Queues, maintaining a recent N items list (e.g. latest 100 comments). |
| Set | SADD, SISMEMBER, SINTER | Unique items, fast membership check. Tags, friends in common (intersection). |
| Sorted Set (ZSet) | ZADD, ZRANGE, ZRANK | Leaderboards, time-ordered streams, rate limiter sliding windows. Each item has a score. |
| Stream | XADD, XREADGROUP | Append-only log with consumer groups — Kafka-lite for event sourcing. |
| HyperLogLog | PFADD, PFCOUNT | Approximate unique counts (e.g., millions of unique IP visitors) using only 12KB memory. |
| Bitmap | SETBIT, GETBIT | Daily active user flags, feature flags. Extremely memory efficient. |
| Geo | GEOADD, GEORADIUS | Location-based queries, finding nearby drivers or stores. |
Caching Patterns
Rendering diagram…
1. Cache-Aside (Lazy Loading) — your pattern
The application is responsible for reading and writing to both storage and cache. Best for general read-heavy workloads.
func GetEvent(ctx context.Context, id string) (*Event, error) {
// 1. Try cache
if data, err := redis.Get(ctx, "event:"+id).Result(); err == nil {
var e Event; json.Unmarshal([]byte(data), &e); return &e, nil
}
// 2. Cache miss — hit DB
e, err := db.GetEvent(ctx, id)
if err != nil { return nil, err }
// 3. Write back to cache with TTL
data, _ := json.Marshal(e)
redis.Set(ctx, "event:"+id, data, 60*time.Second)
return e, nil
}2. Write-Through
App writes data to cache and DB synchronously in one transaction. Pro: Data is never stale. Con: Slower writes.
3. Write-Behind (Write-Back)
App writes to cache only and returns immediately. An asynchronous process later flushes the cache to the DB. Pro: Extremely fast writes. Con: Risk of data loss if cache crashes before flush.
4. Read-Through
The application asks the cache for data. If missing, a cache provider automatically fetches it from the DB. Less app code.
TTL & Eviction Policies
Memory is expensive. You must manage how keys are removed.
- TTL (Time To Live): Always set a TTL on cached data (
SET key val EX 60). Never let caches grow unbounded. - maxmemory: The memory limit configured in redis.conf.
- maxmemory-policy: What Redis does when it hits the limit:
allkeys-lru: Evict least recently used keys out of all keys. (Standard for caches).volatile-lru: Evict least recently used keys among those with an expiration set.allkeys-lfu: Evict least frequently used keys.noeviction: Return errors on write. (Standard if Redis is used as a primary database/queue).
Common Caching Pitfalls
Rendering diagram…
- Cache Stampede (Thundering Herd): A hot key (e.g., homepage data) expires. Suddenly 1,000 requests hit the DB at once, bringing it down.
Solution: Use a Mutex (single-flight) so only one request queries the DB while others wait, or use probabilistic early expiration (refresh slightly before expiry). - Cache Penetration: Malicious users query for non-existent IDs. Cache misses, hits DB, returns nothing. Repeat.
Solution: Cache the empty result (NULL) with a short TTL, or use a Bloom Filter before querying the DB. - Avalanche: All cache keys expire at the exact same time (e.g. nightly cron jobs), flooding the DB.
Solution: Add random Jitter to TTLs (e.g., base TTL 60s + random(0-10s)). - Inconsistency on writes: If you update DB then update Cache, a crash between leaves a stale cache forever.
Solution: Use Delete-on-write (invalidate cache). It's safer to delete the cache and let the next read repopulate it than to try to keep it perfectly updated. - Big Keys: Storing 50MB in one key blocks Redis (since it's single threaded) during serialization.
Solution: Split data, or store large JSON/Images in S3 and cache the URL. - Blocking commands: Running
KEYS *in production blocks all other commands.
Solution: UseSCANwhich iterates in chunks.
Distributed Redis & Concurrency
Distributed Locks
When multiple microservices need exclusive access to a resource.
// Acquire Lock (Set if Not eXists, with Expiry to prevent permanent deadlock)
SET lock:resource123 "unique_worker_id" NX EX 30
// Release Lock (Must check ownership first to avoid deleting another worker's lock)
// Handled via Lua script for atomicity:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
endNote: For highly critical locks across multiple nodes, the Redlock algorithm is used, though it has complex distributed systems caveats.
Pub/Sub vs Streams
- Pub/Sub (PUBLISH/SUBSCRIBE): Fire and forget. If a subscriber is offline, the message is lost. Great for live real-time notifications.
- Streams (XADD/XREAD): Persistent append-only logs. Consumer groups track offsets. If a worker dies, another can resume. Similar to Kafka.
Persistence (RDB vs AOF)
If Redis crashes, does it lose data?
- RDB (Redis Database Snapshot): Point-in-time snapshot to disk every X minutes. Fast to restart, but loses recent data on crash.
- AOF (Append Only File): Logs every write operation. Slower, larger file, but no data loss. Usually, production runs both.
High Availability
- Redis Sentinel: Monitors master and replicas. Automatically promotes a replica to master if the master goes down.
- Redis Cluster: Automatically shards/partitions data across multiple masters. Used when data is larger than RAM on a single machine.
Redis vs Memcached
| Feature | Redis | Memcached |
|---|---|---|
| Data types | Strings, Hashes, Lists, Sets, ZSets, Streams | Strings only |
| Persistence | Yes (RDB/AOF) | No (restarts empty) |
| Features | Pub/Sub, Lua scripting, Transactions | Bare-bones caching |
| Threading | Single-threaded execution | Multi-threaded |
| Best for | Complex data manipulation, queues, state, HA | Pure massive-scale string caching |
Interview Quick Reference
| Topic | Key Points to Mention |
|---|---|
| Data Types | ZSets for leaderboards, HyperLogLog for counting, Hashes for objects. |
| Cache-aside | Check cache → hit DB on miss → populate cache. Delete cache on write. |
| Pitfalls | Stampede (thundering herd), Penetration (cache NULLs), Avalanche (add Jitter). |
| Eviction | allkeys-lru, always set TTLs. |
| Single Threaded | No race conditions on basic commands, but long operations (KEYS *) block the world. |