Redis is a single-threaded, in-memory data-structure server. Single-threaded for command execution means one slow command stalls everyone — so the senior story is all about O(N) commands, memory, persistence fork pauses, and the replication/HA topology. It's not "a cache"; it's a toolbox of data structures with optional durability.
1. Why single-threaded matters
- Commands run on one thread in an event loop → atomic by nature (no locks between commands), but one O(N) command blocks all clients.
- Redis 6+ adds threaded I/O (socket read/write only); command execution stays single-threaded. Redis 7 keeps this model.
- Implication: never run
KEYS *, bigSMEMBERS/HGETALL, or a long Lua script in prod — they freeze the loop.
2. Data types & when to use them
| Type | Use / key commands |
|---|---|
| String | Cache value, counter. SET/GET, INCR, SETEX, SETNX, GETSET. |
| Hash | Object/fields. HSET/HGET/HGETALL, HINCRBY. Memory-efficient for small objects. |
| List | Queue/stack. LPUSH/RPUSH, LPOP/RPOP, BLPOP (blocking), LRANGE. |
| Set | Unique members, set ops. SADD, SISMEMBER, SINTER/SUNION. |
| Sorted Set (ZSet) | Leaderboard, priority, time index. ZADD, ZRANGE, ZRANGEBYSCORE, ZRANK. |
| Stream | Append-only log + consumer groups (Kafka-lite). XADD, XREADGROUP, XACK. |
| Bitmap / HyperLogLog | Flags / approx-cardinality. SETBIT, BITCOUNT; PFADD/PFCOUNT (~0.81% error, 12KB). |
| Geo | Geospatial (zset under the hood). GEOADD, GEOSEARCH. |
3. Keys, TTL & expiry
EXPIRE key 60 ; PEXPIRE key 1500 ; TTL key ; PERSIST key SET key val EX 60 NX # set if-absent with TTL (atomic) SCAN 0 MATCH user:* COUNT 100 # cursor iteration — NEVER KEYS * in prod
- Expiry is lazy (on access) + active (background sampling) — an expired key can briefly linger in memory until reaped.
- On a replica, keys don't expire on their own — the primary sends
DELon expiry (avoids replicas diverging).
KEYS * is a production footgun
KEYS is O(N) over the whole keyspace and blocks the single thread — a multi-second
freeze on millions of keys. Always SCAN (cursor, non-blocking, bounded work).4. Memory & eviction
CONFIG GET maxmemory ; CONFIG SET maxmemory 4gb CONFIG SET maxmemory-policy allkeys-lru INFO memory # used_memory, mem_fragmentation_ratio, maxmemory_policy
| Policy | Behaviour |
|---|---|
noeviction | Reject writes when full (default). Good for a durable store, bad for a cache. |
allkeys-lru / lfu | Evict least-recently / least-frequently used across all keys. Cache default. |
volatile-lru / ttl / random | Evict only among keys with a TTL. |
cache with noeviction = outage
A cache using
noeviction stops accepting writes when full instead of dropping cold
keys. Use allkeys-lru/lfu and set maxmemory below the
container limit (else the kernel OOM-kills the process).5. Persistence — RDB vs AOF
| RDB | AOF | |
|---|---|---|
| What | Point-in-time snapshot (fork + dump) | Append-only log of write commands |
| Recovery | Faster load, lose data since last snapshot | Less data loss (fsync everysec), slower replay |
| Cost | Fork = copy-on-write memory spike | Disk-heavy; periodic rewrite/compaction |
appendfsync everysec is the balanced default (≤1s loss). Many run both
(AOF for durability + RDB for fast restart). BGSAVE/BGREWRITEAOF fork in the
background — the fork's copy-on-write can double memory under heavy writes.
6. Transactions, Lua & pipelining
- MULTI/EXEC — queue commands, run atomically (no interleaving). Not rollback-on-error; WATCH adds optimistic locking (CAS) — abort if a watched key changed.
- Lua scripts (
EVAL/EVALSHA) run atomically server-side — ideal for read-modify-write (rate limiters, locks). Keep them short (they block the loop). - Pipelining — send many commands without waiting for each reply → fewer round trips, big throughput win. Not atomic; just batched.
7. Patterns (the interview favourites)
- Cache-aside: read cache → miss → read DB →
SETwith TTL. Handle stampedes with jittered TTL / locks / request coalescing. - Distributed lock:
SET key token NX PX 30000; release via a Lua compare-and-del (only if token matches). For HA correctness, Redlock — but it's debated; a single instance + fencing token is often enough. - Rate limiting:
INCR+EXPIRE(fixed window), or a Lua sliding-window with a ZSet. - Leaderboard: ZSet
ZADD/ZREVRANGE. - Queue / stream: Lists (
BLPOP) for simple; Streams + consumer groups for ack/replay.
naive SET NX lock is unsafe
Locking with
SETNX then a plain DEL can delete someone else's
lock after your TTL expired. Store a unique token and release with a Lua check-and-delete; add a
fencing token if the protected resource supports it.8. Replication, Sentinel & Cluster
- Replication — async primary → replicas; replicas serve reads (can be stale).
WAITcan require N replicas ack a write (not full sync durability). - Sentinel — monitors + automatic failover for a single-primary setup (promotes a replica, updates clients). HA, not sharding.
- Cluster — sharding across nodes via 16384 hash slots (
CRC16(key) % 16384); each primary owns a slot range + has replicas. Multi-key ops must share a slot (use hash tags{user1}:profile).
async replication can lose acked writes
A write acked by the primary may not have reached a replica before failover — small data-loss
window.
WAIT reduces it but isn't synchronous durability. Know this trade-off.9. Performance & diagnosis
redis-cli --latency / --latency-history # round-trip latency redis-cli SLOWLOG GET 20 # slowest commands + args redis-cli LATENCY DOCTOR # human latency analysis redis-cli INFO commandstats # usec_per_call per command redis-cli --bigkeys / --hotkeys # memory hogs / hot keys redis-cli INFO clients # blocked_clients, connected_clients
Top causes of latency: O(N) commands on big keys, fork pauses (RDB/AOF rewrite), swap (never
let Redis swap), big-value (de)serialization, MONITOR left running, THP enabled on the
host. Use UNLINK (async free) not DEL for big keys.
10. Senior interview Q&A
- Why is single-threaded a feature, not a bug?No lock contention, atomic commands, predictable; bottleneck is memory/network not CPU. Downside: one O(N) command blocks all — so avoid KEYS/big-key ops. I/O is threaded in 6+.
- RDB vs AOF?RDB = periodic snapshot (fast restart, more loss). AOF = command log (less loss with everysec, slower replay). Often run both.
- How does eviction work / which policy for a cache?When maxmemory hit, evict per maxmemory-policy. Cache → allkeys-lru/lfu; durable store → noeviction (but then full = write errors).
- Is MULTI/EXEC a real transaction?Atomic + isolated, but no rollback on logic errors. WATCH gives optimistic CAS. For atomic read-modify-write, prefer a Lua script.
- How do you build a safe distributed lock?SET key token NX PX ttl; release via Lua that deletes only if token matches; add a fencing token. Redlock for multi-node (debated).
- Sentinel vs Cluster?Sentinel = HA/failover for single-primary (no sharding). Cluster = sharding via 16384 hash slots + per-shard replicas.
- How does Cluster shard keys?CRC16(key) % 16384 → slot → node. Multi-key ops must hit one slot; force co-location with hash tags {tag}.
- Can Redis lose data?Yes — async replication + failover has a loss window; RDB loses since last snapshot. WAIT/AOF reduce it. It's not a system of record by default.
- How do you prevent a cache stampede?Jittered TTLs, a short lock so one request rebuilds, request coalescing, or stale-while-revalidate.
- Why did latency spike with no traffic change?Usually a fork pause (BGSAVE/AOF rewrite), an O(N) command on a big key, swap, or THP. Check SLOWLOG + LATENCY DOCTOR.
- DEL vs UNLINK?DEL frees synchronously (blocks on a huge key); UNLINK frees in a background thread.
- Redis vs Memcached?Memcached = simple multi-threaded LRU cache. Redis = rich data structures, persistence, replication, pub/sub, scripting, cluster.
- When NOT to use Redis?As the only system of record for critical data (loss window), for datasets that don't fit RAM affordably, or for complex queries/joins (use a DB).