Redis 高频面试题
本节汇总了中高级职位的 Redis 高频面试题,涵盖从基础架构到生产问题的深度解析。
基础与核心原理
Q1: Redis 为什么这么快?
Redis 的高性能源于以下几个关键因素:
内存操作:所有数据存储在内存中,读写速度可达 10 万+ QPS。内存访问延迟是纳秒级,而磁盘是毫秒级,差距达 10 万倍。
单线程模型:Redis 核心命令执行采用单线程,避免了多线程的上下文切换开销、锁竞争问题。单线程并不意味着慢,反而因为没有锁开销,在单核上运行效率极高。
高效数据结构:Redis 针对不同场景设计了专用数据结构:
- SDS(简单动态字符串):O(1) 获取长度,避免缓冲区溢出
- 哈希表:使用渐进式 rehash,避免一次性迁移阻塞
- 跳表:O(logN) 的范围查询,比平衡树实现简单
- Listpack/IntSet:小数据量时的紧凑存储
I/O 多路复用:基于 epoll/kqueue 实现,单线程可以同时处理大量连接,不会阻塞在某个连接的 I/O 上。
Redis 6.0 多线程 I/O:网络读写部分使用多线程加速,但核心命令执行仍是单线程。在网络 I/O 成为瓶颈时,可以显著提升吞吐量。
Q2: 单线程 Redis 能利用多核吗?
核心命令执行:主线程在单个 CPU 核心上运行,无法利用多核。这是设计选择,因为 Redis 的瓶颈通常在网络 I/O 或内存带宽,而非 CPU 计算。
Redis 6.0+ 多线程 I/O:
io-threads:网络读写线程数io-threads-do-reads:是否在 I/O 线程中执行读操作- 适合网络带宽成为瓶颈的高并发场景
后台任务:以下操作由子线程或子进程执行,可以并行:
- AOF 后台刷盘
UNLINK异步删除大键BGSAVE后台 RDB 快照- 惰性删除和过期键清理
建议:在多核服务器上,可以部署多个 Redis 实例(不同端口),每个实例绑定不同 CPU 核心。
Q3: Redis 为什么用跳表而不用 B+ 树?
跳表的优势:
| 特性 | 跳表 | B+ 树 |
|---|---|---|
| 实现复杂度 | 简单 | 复杂 |
| 范围查询 | 高效 | 高效 |
| 内存占用 | 指针多 | 更紧凑 |
| 并发支持 | 无锁友好 | 需要锁 |
| 修改成本 | 低(局部) | 可能触发分裂 |
Redis 选择跳表的原因:
- 实现简单:跳表的插入、删除、查找逻辑清晰,易于维护
- 内存友好:Redis 是内存数据库,跳表在内存中的表现与 B+ 树差异不大
- 范围查询高效:跳表可以像链表一样遍历,支持 ZRANGE 等命令
- 无锁并发:跳表的局部修改特性更适合并发场景
B+ 树更适合磁盘存储:B+ 树设计目标是减少磁盘 I/O 次数,每个节点存储更多数据。但在内存中,这个优势不明显。
持久化与一致性
Q4: RDB 和 AOF 如何选择?
RDB(快照):
- 优点:文件紧凑,恢复速度快,适合备份
- 缺点:可能丢失几分钟数据,fork 大数据集时可能阻塞
- 适用:可接受数据丢失的缓存场景、灾备恢复
AOF(日志):
- 优点:数据更安全(最多丢失 1 秒),文件可读
- 缺点:文件体积大,恢复较慢
- 适用:数据安全要求高的场景
混合持久化(Redis 4.0+,推荐):
开启方式:aof-use-rdb-preamble yes
工作原理:
1. AOF 重写时,先写入 RDB 格式的快照
2. 然后追加增量的 AOF 命令
3. 恢复时先加载 RDB 部分,再执行 AOF 命令
优势:结合 RDB 的快速恢复和 AOF 的数据安全
生产配置建议:
# 开启 AOF
appendonly yes
appendfsync everysec
# 开启混合持久化
aof-use-rdb-preamble yes
# 保留 RDB 用于备份
save 900 1
save 300 10
Q5: AOF 重写的原理是什么?
为什么需要重写:
- AOF 记录所有写命令,文件会越来越大
- 很多命令是冗余的(如多次 SET 同一个 key)
重写流程:
1. 客户端执行 BGREWRITEAOF
2. Redis 主进程 fork 子进程
3. 子进程遍历内存数据,生成新 AOF 文件
- 只记录当前状态,不记录历史命令
- 例如:SET a 1; SET a 2; SET a 3 → 只记录 SET a 3
4. 主进程继续接收新命令,写入重写缓冲区
5. 子进程完成后,主进程追加重写缓冲区内容
6. 原子性地替换旧 AOF 文件
Redis 7.0 Multi-part AOF:
- 不再使用单个临时文件
- 分为 Base AOF(基础文件)和 Incremental AOF(增量文件)
- 减少了重写期间的内存压力和文件切换风险
Q6: 如何保证 Redis 和数据库的数据一致性?
Cache Aside 模式(推荐):
读操作:
1. 先读缓存
2. 缓存命中 → 返回
3. 缓存未命中 → 读数据库 → 写入缓存 → 返回
写操作:
1. 更新数据库
2. 删除缓存(不是更新缓存)
为什么删除而不是更新缓存:
- 并发更新可能导致脏数据
- 删除后由读请求懒加载最新数据
延时双删策略:
def update_data(key, value):
# 1. 删除缓存
redis.delete(key)
# 2. 更新数据库
db.update(key, value)
# 3. 延时后再删除(缓解主从延迟问题)
time.sleep(500) # 毫秒
redis.delete(key)
Binlog 订阅方案:
- 使用 Canal 等工具订阅 MySQL Binlog
- 异步监听数据变更,删除对应缓存
- 优点:业务代码无侵入,保证最终一致性
高可用与集群
Q7: 哨兵(Sentinel)和集群(Cluster)如何选择?
哨兵模式:
架构:1 主 + N 从 + 3 哨兵
特点:数据全量复制,写压力在单节点
适用:读多写少,单机写能力足够
集群模式:
架构:多主 + 多从,数据分片存储
特点:写压力分散到多个节点
适用:海量数据,高写并发
对比:
| 维度 | 哨兵 | 集群 |
|---|---|---|
| 数据分片 | 不支持 | 支持 |
| 写扩展 | 不支持 | 支持 |
| 部署复杂度 | 中 | 高 |
| 最少节点 | 4(1主1从2哨兵) | 6(3主3从) |
| 多键操作 | 支持 | 受限(需同槽) |
| 事务 | 支持 | 受限 |
Q8: 为什么 Cluster 槽数量是 16384?
不是 65536 的原因:
- 节点间通过心跳包交换槽位信息
- 槽位信息以 Bitmap 形式携带
- 16384 个槽需要 2KB 的 Bitmap
- 如果是 65536 个槽,需要 8KB Bitmap
- 心跳包过大影响网络性能
16384 是否够用:
- 通常节点数在 1000 以内最佳
- 16384 个槽分配给 1000 个节点,每个约 16 个槽
- 完全满足实际需求
Q9: Cluster 如何保证高可用?
故障检测:
- 节点间互相发送 PING/PONG
- 超过
cluster-node-timeout未响应 → 标记为 PFAIL(可能下线) - 多数 Master 确认 → 标记为 FAIL(真正下线)
故障转移:
- 下线 Master 的 Replica 发起选举
- 向其他 Master 请求投票
- 获得多数票后升级为 Master
- 广播新配置
一致性权衡:
- 异步复制可能导致数据丢失
- 使用
WAIT命令等待复制确认 - 网络分区时可能出现短暂双主
缓存生产故障
Q10: 什么是缓存雪崩、击穿、穿透?
缓存雪崩:
- 现象:大量 Key 同时过期,请求全部打到数据库
- 解决:
- 过期时间加随机偏移
- 设置逻辑过期,后台异步刷新
- 使用互斥锁重建缓存
缓存击穿:
- 现象:热点 Key 过期瞬间,大量并发请求穿透到数据库
- 解决:
- 使用分布式锁,只让一个请求重建缓存
- 永不过期,通过逻辑过期控制更新
- 提前异步刷新即将过期的热点数据
缓存穿透:
- 现象:查询不存在的数据,每次都穿透到数据库
- 解决:
- 缓存空值(设置较短过期时间)
- 使用布隆过滤器预判断
- 参数校验,拦截非法请求
# 综合解决方案
def get_with_protection(key):
# 布隆过滤器预判断
if not bloom_filter.exists(key):
return None
# 尝试从缓存获取
value = redis.get(key)
if value is not None:
return value
# 分布式锁保护
lock_key = f"lock:{key}"
if redis.set(lock_key, "1", nx=True, ex=10):
try:
# 双重检查
value = redis.get(key)
if value is not None:
return value
# 查询数据库
value = db.query(key)
if value:
# 随机过期时间防止雪崩
ttl = 3600 + random.randint(0, 300)
redis.setex(key, ttl, value)
else:
# 缓存空值防止穿透
redis.setex(key, 60, "NULL")
return value
finally:
redis.delete(lock_key)
else:
# 等待并重试
time.sleep(0.1)
return get_with_protection(key)
Q11: 什么是 Big Key?如何处理?
定义:
- String 类型:值超过 10KB
- 集合类型:元素数量超过 5000
危害:
- 网络传输慢,阻塞其他请求
- 删除操作阻塞主线程
- 主从同步延迟增加
- 内存分布不均
发现方法:
# 使用 redis-cli 扫描
redis-cli --bigkeys
# 查看单个键内存
MEMORY USAGE mykey
处理方法:
# 方式一:异步删除(Redis 4.0+)
UNLINK bigkey
# 方式二:分批删除 Hash
HSCAN big:hash 0
# 然后分批 HDEL
# 方式三:分批删除 List
LTRIM big:list 0 -1001
# 每次删除部分
# 方式四:渐进式删除
while (LLEN big:list > 0) {
RPOP big:list
}
预防措施:
- 设计阶段拆分大键
- 控制集合元素数量
- 监控告警
Redis 7.4 新特性
Q12: Redis 7.4 Hash 字段过期有什么意义?
之前的痛点:
- Hash 只能整体设置过期时间
- 实现字段过期需要复杂逻辑(如使用额外 Key 或时间戳后缀)
新特性:
# 设置字段级过期
HSET user:1001 name "张三" session "abc123"
HEXPIRE user:1001 1800 FIELDS 1 session # session 30分钟后过期
# 查看字段剩余时间
HTTL user:1001 FIELDS 1 session
# 应用场景
# 1. 会话管理:用户信息持久化,会话字段自动过期
# 2. 事件追踪:记录最近 N 小时的事件,自动清理
# 3. 临时属性:用户临时状态,无需手动清理
优势:
- 减少业务代码复杂度
- 避免创建大量小 Key
- 内存效率更高
性能优化
Q13: 如何优化 Redis 内存使用?
数据结构选择:
- 存储对象使用 Hash 而非多个 String
- 小数据量自动使用 Listpack/IntSet 压缩
键名设计:
- 简短但有意义
- 避免过长的键名
编码优化:
# 调整压缩阈值
hash-max-listpack-entries 512
hash-max-listpack-value 64
zset-max-listpack-entries 128
set-max-intset-entries 512
过期策略:
- 设置合理的过期时间
- 避免大量键同时过期
内存碎片整理:
activedefrag yes
active-defrag-threshold-lower 10
Q14: 如何监控 Redis 性能?
核心指标:
| 指标 | 命令 | 说明 |
|---|---|---|
| 内存使用 | INFO memory | used_memory, 碎片率 |
| QPS | INFO stats | instantaneous_ops_per_sec |
| 连接数 | INFO clients | connected_clients |
| 缓存命中率 | INFO stats | keyspace_hits/misses |
| 慢查询 | SLOWLOG GET | 执行时间长的命令 |
告警阈值建议:
- 内存使用 > 80%
- 缓存命中率 < 80%
- 慢查询数量突增
- 连接数接近 maxclients
小结
本节汇总了 Redis 面试的高频问题,涵盖:
- 基础原理:为什么快、单线程、数据结构选择
- 持久化:RDB/AOF 选择、重写原理
- 高可用:哨兵 vs 集群、故障转移
- 缓存问题:雪崩、击穿、穿透解决方案
- 运维实践:Big Key 处理、内存优化、性能监控
建议结合实际项目经验深入理解这些问题,在面试中展示系统性思维和实战能力。