跳到主要内容

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 选择跳表的原因

  1. 实现简单:跳表的插入、删除、查找逻辑清晰,易于维护
  2. 内存友好:Redis 是内存数据库,跳表在内存中的表现与 B+ 树差异不大
  3. 范围查询高效:跳表可以像链表一样遍历,支持 ZRANGE 等命令
  4. 无锁并发:跳表的局部修改特性更适合并发场景

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(真正下线)

故障转移

  1. 下线 Master 的 Replica 发起选举
  2. 向其他 Master 请求投票
  3. 获得多数票后升级为 Master
  4. 广播新配置

一致性权衡

  • 异步复制可能导致数据丢失
  • 使用 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 memoryused_memory, 碎片率
QPSINFO statsinstantaneous_ops_per_sec
连接数INFO clientsconnected_clients
缓存命中率INFO statskeyspace_hits/misses
慢查询SLOWLOG GET执行时间长的命令

告警阈值建议

  • 内存使用 > 80%
  • 缓存命中率 < 80%
  • 慢查询数量突增
  • 连接数接近 maxclients

小结

本节汇总了 Redis 面试的高频问题,涵盖:

  1. 基础原理:为什么快、单线程、数据结构选择
  2. 持久化:RDB/AOF 选择、重写原理
  3. 高可用:哨兵 vs 集群、故障转移
  4. 缓存问题:雪崩、击穿、穿透解决方案
  5. 运维实践:Big Key 处理、内存优化、性能监控

建议结合实际项目经验深入理解这些问题,在面试中展示系统性思维和实战能力。

参考资料