DBMS deep dive
Redis — Use Cases & Patterns: deep dive
Redis is a set of in-memory data structures you reach over the network, and each structure maps cleanly to a production use case. The skill interviewers check is picking the right structure and command for the job, and knowing what breaks when memory, persistence or atomicity are pushed.
Data structure to use case map
| Structure | Use case | Key commands | Complexity |
|---|---|---|---|
| String | Cache entry, counter, distributed lock | SET key val EX 60 NX, GET, INCR | O(1) |
| Hash | Session or user profile fields | HSET, HGET, HINCRBY | O(1) per field |
| List | Simple job queue, recent-activity feed | LPUSH, BRPOP / BLMOVE, LTRIM | O(1) at the ends |
| Set | Unique visitors per day, tags, mutual friends | SADD, SISMEMBER, SINTER | O(1) membership |
| Sorted set | Leaderboard, sliding-window rate limit, delayed jobs | ZADD, ZREVRANGE, ZRANGEBYSCORE, ZREMRANGEBYSCORE | O(log n) |
| Stream | Durable event log with consumer groups | XADD, XREADGROUP, XACK | O(1) append |
| HyperLogLog | Approximate distinct count (~0.81% error, 12 KB) | PFADD, PFCOUNT | O(1) |
| Bitmap | Daily active flags per user ID | SETBIT, BITCOUNT | O(1) per bit |
| Geo | Nearby drivers or stores | GEOADD, GEOSEARCH | O(log n + m) |
Worked example: a cache that survives a traffic spike
In cache-aside, the app reads Redis first. On a miss it loads the value from the database and writes it to Redis with a TTL. On writes it updates the database and then deletes the key rather than overwriting it, which avoids racing writers leaving a stale value. The danger is a cache stampede: a hot key expires and thousands of requests hit the database at once. Two fixes help. Add random jitter to TTLs so keys do not expire together, and let only one request rebuild the value while the others briefly wait or serve the stale copy.
# Cache-aside with jittered TTL (300s + random 0-60s chosen by the app)
GET product:42
SET product:42 "{...json...}" EX 337
# Rebuild lock so only one worker refills a hot key
SET lock:product:42 "worker-7f3a" NX PX 5000 # OK = you own it, nil = someone else does
# Safe release: delete only if you still own it (atomic Lua script)
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:product:42 worker-7f3a
# Leaderboard: top 3 and one player's rank (0-based)
ZINCRBY lb:weekly 50 "asha"
ZREVRANGE lb:weekly 0 2 WITHSCORES
ZREVRANK lb:weekly "asha"
# Reliable queue: move the job to a processing list atomically
LPUSH jobs '{"id":981}'
BLMOVE jobs jobs:processing RIGHT LEFT 5Why the Lua script for releasing the lock? A plain DEL could delete a lock that already expired and was taken by another worker. Comparing the token and deleting must happen as one atomic step. Scripts run atomically because Redis executes commands on a single thread. Since Redis 6, I/O threads can handle networking, but command execution is still serial.
Sliding-window rate limiting with a sorted set
A fixed-window counter (INCR plus EXPIRE) lets a client send up to twice the limit across a window boundary. A sliding window stores each request's timestamp as a sorted-set member. Remove members older than the window with ZREMRANGEBYSCORE, count what is left with ZCARD, and add the new request with ZADD, all in one MULTI block or Lua script. It is more accurate, at the cost of memory per request.
Memory, eviction and persistence trade-offs
- Set
maxmemoryand a policy.allkeys-lruorallkeys-lfusuit pure caches.volatile-*policies evict only keys that have a TTL.noevictionreturns errors on writes, which is what you want for data you cannot lose. - RDB snapshots fork the process. Copy-on-write can briefly need much more memory on write-heavy instances.
- Replication is asynchronous, so a failover can lose recent writes. That is one reason Redis locks are advisory for efficiency, not a correctness guarantee. Use fencing tokens if correctness depends on the lock.
- Pub/Sub is fire-and-forget: offline subscribers miss messages. Use Streams when consumers must catch up.
For how Redis fits next to a primary database, see SQL vs NoSQL.
Common mistakes
- Big keys, such as a hash with millions of fields or a 50 MB string. Deleting or reading one blocks the server. Use
UNLINKfor background deletion and split big keys into smaller ones. - Running
KEYS *orSMEMBERSon huge sets in production instead ofSCANandSSCAN. - Setting the value and the TTL in two separate commands. A crash in between leaves a key that never expires. Use
SET ... EX. - Sending many round trips per request instead of pipelining or using a multi-key command such as
MGET. - Assuming multi-key commands work across a Redis Cluster. Keys must share a hash slot, which you can force with hash tags like
{user:42}:cart.
Interview questions
›How do MULTI/EXEC transactions differ from SQL transactions?
MULTI queues commands, and EXEC runs them back to back with nothing interleaved. There is no rollback, though: if one command fails at runtime, the others still apply. WATCH adds optimistic concurrency by aborting EXEC if a watched key changed.
›How would you design a real-time leaderboard for 10 million players?
Use a sorted set with score = points. ZINCRBY updates are O(log n), ZREVRANGE fetches the top N, and ZREVRANK gives a player's rank. Shard by season or region if one key gets too large, and store player profiles in hashes or the main database.
›What happens when Redis reaches maxmemory?
It applies the configured eviction policy, sampling keys to approximate LRU or LFU. With noeviction, commands that would add memory return an OOM error, while reads keep working.
›Is Redlock safe for distributed locking?
It is debated. Redlock depends on bounded clock drift and process pauses, so a GC pause can let two clients believe they hold the lock. For efficiency locks it is fine. For correctness, use fencing tokens checked by the protected resource, or a consensus system like etcd or ZooKeeper.
›When would you use Redis Streams over a List-based queue?
Use Streams when you need consumer groups, per-message acknowledgement, re-delivery of pending messages after a consumer crash, and the ability to replay history. A List queue is simpler but has no built-in acknowledgement or fan-out.
