Redis Interview: Model, Patterns and Failures
Single-threaded model, data types, persistence, eviction, cluster/sentinel, and the caching failure modes (penetration/breakdown/avalanche) interviewers drill on.
Model and data types
Q1: Single-threaded but fast — why?
Everything runs in one event loop (epoll multiplexing) after 6.0 added thread pools only for network IO, not for command execution — avoiding locks, context switches and race conditions. Speed comes from in-memory + efficient data structures, not threads.
Q2: Type → structure → use case.
| Type | Structure | Classic use |
|---|---|---|
| String | SDS | 计数器、缓存对象 JSON、分布式锁 |
| List | quicklist(双向链表+ziplist) | 消息队列、最新 N 条 |
| Hash | ziplist/hashtable | 对象属性(比 String 存 JSON 省空间) |
| Set | intset/hashtable | 标签、共同好友 |
| Zset | ziplist/skiplist+hashtable | 排行榜、延时队列 |
| Stream | radix tree | 消息流 + 消费组(5.0+) |
Persistence and eviction
Q3: RDB vs AOF.
RDB is a point-in-time snapshot — compact, fast restart, but loses data since the last snapshot. AOF appends every write command — richer durability, fsync policy (always/everysec/no) controls the trade; AOF rewrite compacts it. Modern Redis 4.0+ supports hybrid: AOF base file is RDB-written, then commands append.
Q4: Eviction policies — which for which workload?
- noeviction: writes error once full — for databases you must never lose silently
- allkeys-lru: classic cache workload
- volatile-lru: mixed instance, only keys with TTL participate
- allkeys-lfu: skewed access (few hot keys) — Redis 4+
Failures: penetration / breakdown / avalanche
| Failure | Cause | Fix |
|---|---|---|
| Cache penetration 缓存穿透 | 查询不存在的 key,每次都打到 DB(恶意攻击/脏 ID) | 布隆过滤器 / 查不到也缓存空值(短 TTL) |
| Cache breakdown 缓存击穿 | 单个热 key 过期瞬间,海量请求打到 DB | 热 key 永不过期 + 后台刷新 / 互斥锁重建 |
| Cache avalanche 缓存雪崩 | 大量 key 同时过期 or 缓存整体宕机 | 过期时间加随机抖动 / Redis 集群 + Hystrix 限流降级 |
# 互斥锁防击穿(伪代码)
func get(ctx, key):
v = redis.get(key)
if v == nil:
if redis.setnx(lock(key), 1, TTL 10s): # 只有一个请求去重建
v = db.query(key)
redis.set(key, v, ttl)
redis.del(lock(key))
else:
sleep(50ms); return get(ctx, key) # 其他请求稍后重试
return v
Q5: Distributed lock — what can go wrong?
- No expire → holder crash → lock stuck forever: always SET key val NX PX 30000
- Timeout while still working → someone else grabs it → two holders: watch-dog renewal thread (Redisson)
- Unlocking someone else's lock: value = unique id, delete with a Lua CAS check
- Failover: master dies before replicating the lock — Redlock raises the bar, still contested (Martin Kleppmann's critique)
模型与数据类型
Q1:单线程为什么还那么快?
6.0 之后引入的线程池只做网络 IO,命令执行仍是单线程事件循环(epoll 多路复用)——天然没有锁、上下文切换和竞态。快是因为「内存 + 高效数据结构」,不是因为多线程。
Q2:类型 → 结构 → 场景。
| 类型 | 底层结构 | 典型用途 |
|---|---|---|
| String | SDS | 计数器、缓存对象 JSON、分布式锁 |
| List | quicklist(双向链表+ziplist) | 消息队列、最新 N 条 |
| Hash | ziplist/hashtable | 对象属性(比 String 存 JSON 省空间) |
| Set | intset/hashtable | 标签、共同好友 |
| Zset | ziplist/skiplist+hashtable | 排行榜、延时队列 |
| Stream | radix tree | 消息流 + 消费组(5.0+) |
持久化与淘汰
Q3:RDB 与 AOF。
RDB 是时间点快照——体积小、重启快,但丢最近一次快照后的数据。AOF 追加每条写命令——持久性更强,fsync 策略(always/everysec/no)决定性能与安全的平衡,rewrite 会压缩。4.0+ 支持混合:AOF 基础段用 RDB 写入,之后追加命令。
Q4:淘汰策略怎么选?
- noeviction:满了直接报错——绝不能悄悄丢数据的场景
- allkeys-lru:经典缓存场景首选
- volatile-lru:实例里缓存和持久数据混存时,只有带 TTL 的参与淘汰
- allkeys-lfu:访问倾斜严重(少量热 key)时用,4.0+
三大经典故障:穿透 / 击穿 / 雪崩
| 故障 | 成因 | 解法 |
|---|---|---|
| Cache penetration 缓存穿透 | 查询不存在的 key,每次都打到 DB(恶意攻击/脏 ID) | 布隆过滤器 / 查不到也缓存空值(短 TTL) |
| Cache breakdown 缓存击穿 | 单个热 key 过期瞬间,海量请求打到 DB | 热 key 永不过期 + 后台刷新 / 互斥锁重建 |
| Cache avalanche 缓存雪崩 | 大量 key 同时过期 or 缓存整体宕机 | 过期时间加随机抖动 / Redis 集群 + Hystrix 限流降级 |
# 互斥锁防击穿(伪代码)
func get(ctx, key):
v = redis.get(key)
if v == nil:
if redis.setnx(lock(key), 1, TTL 10s): # 只有一个请求去重建
v = db.query(key)
redis.set(key, v, ttl)
redis.del(lock(key))
else:
sleep(50ms); return get(ctx, key) # 其他请求稍后重试
return v
Q5:分布式锁会出什么问题?
- 不设过期 → 持有者宕机 → 死锁:永远用 SET key val NX PX 30000
- 业务没做完锁就过期 → 别人拿到 → 两个持有者:看门狗续期(Redisson)
- 误删别人的锁:value 存唯一 ID,删除用 Lua 做 CAS 校验
- 主从切换丢锁 → Redlock 提高了门槛,但仍有争议(Martin Kleppmann 那篇著名批评)