【面试特集】Redis 面试题与应用场景

Redis 面试题与应用场景

一、数据结构与基础

Q1: Redis 为什么这么快? ⭐⭐⭐

答案:

  1. 纯内存操作:数据存储在内存中,读写无磁盘 I/O
  2. 单线程模型:避免上下文切换和锁竞争开销
  3. I/O 多路复用:epoll/kqueue 高效处理大量并发连接
  4. 高效数据结构:SDS、跳表、listpack 等专门优化的结构
  5. 6.0+ 多线程 I/O:网络读写并行化,命令执行仍单线程

追问:单线程为什么还能支撑 10万+ QPS?

  • 瓶颈在网络 I/O 而非 CPU
  • 内存操作本身极快(纳秒级)
  • 避免了多线程的锁开销

Q2: Redis 的 String 底层是怎么实现的?

答案:

String 有三种内部编码:

  • int:值为整数且 ≤ long 范围时,直接存储整数值
  • embstr:字符串 ≤ 44 字节,SDS 和 redisObject 一次分配连续内存
  • raw:字符串 > 44 字节,SDS 和 redisObject 分开分配

SDS(Simple Dynamic String)相比 C 字符串的优势:

  • O(1) 获取长度(len 字段)
  • 二进制安全(不以 \0 判断结束)
  • 预分配空间减少内存重分配
  • 兼容部分 C 字符串函数

Q3: Sorted Set 为什么用跳表而不是红黑树? ⭐⭐

答案:

  • 范围查询高效:跳表天然有序,范围查询 O(logN+M),红黑树需要中序遍历
  • 实现简单:跳表代码比红黑树简单得多,易于维护和调试
  • 内存友好:可以通过调整层数灵活平衡时间和空间
  • 并发友好:跳表局部修改即可,红黑树可能需要旋转影响多个节点

实际上 Sorted Set 用的是 skiplist + hashtable 双结构:

  • skiplist:支持有序遍历和范围查询
  • hashtable:O(1) 按 member 查 score

Q4: Redis 各数据类型的底层编码和使用场景?

答案:

类型 底层编码 切换条件 典型场景
String int / embstr / raw 长度 > 44 字节用 raw 缓存、计数器、分布式锁
List listpack / quicklist 元素数 > 128 或单元素 > 64 字节 消息队列、最新列表
Hash listpack / hashtable 字段数 > 128 或值 > 64 字节 对象存储、购物车
Set listpack / hashtable 元素数 > 128 或非整数 标签、好友关系、去重
ZSet listpack / skiplist+hashtable 元素数 > 128 或值 > 64 字节 排行榜、延迟队列

Q5: Redis 的过期键删除策略? ⭐⭐⭐

答案:

Redis 使用两种策略结合:

惰性删除: 访问 key 时检查是否过期,过期则删除。

  • 优点:CPU 友好(只在访问时检查)
  • 缺点:过期 key 不访问则永不删除,内存泄漏

定期删除: 每隔一段时间(默认 100ms)随机抽取一批 key 检查,删除过期的。

  • 每次抽取 20 个,若过期比例 > 25% 则继续抽取
  • 限制每次执行时间,避免阻塞

追问:内存不足时怎么办?

触发内存淘汰策略(maxmemory-policy):

  • noeviction:不淘汰,写入报错(默认)
  • allkeys-lru:淘汰最近最少使用的 key(推荐)
  • volatile-lru:只淘汰设置了过期时间的 key 中 LRU 的
  • allkeys-random:随机淘汰

二、持久化

Q6: RDB 和 AOF 的区别? ⭐⭐⭐

答案:

维度 RDB AOF
原理 定时快照(fork 子进程) 记录每条写命令
文件大小 小(二进制压缩) 大(文本命令)
恢复速度 慢(重放命令)
数据安全 低(两次快照间数据丢失) 高(最多丢失 1 秒)
性能影响 fork 时有短暂 STW 每秒 fsync 影响小
适用场景 备份、快速恢复 数据安全要求高

混合持久化(推荐): AOF 文件前半段是 RDB 快照,后半段是增量 AOF 命令,兼顾恢复速度和数据安全。

Q7: AOF 重写的原理?

答案:

AOF 文件会随时间增大(同一 key 多次修改产生多条命令),重写将其压缩为最小命令集。

过程:

  1. fork 子进程,子进程读取当前内存数据,生成新 AOF 文件
  2. 主进程继续处理请求,新命令写入 AOF 重写缓冲区
  3. 子进程完成后,主进程将缓冲区命令追加到新 AOF 文件
  4. 原子替换旧 AOF 文件

触发条件:

  • auto-aof-rewrite-percentage 100:AOF 文件比上次重写后增长 100%
  • auto-aof-rewrite-min-size 64mb:AOF 文件至少 64MB

三、高可用

Q8: Redis Sentinel 的工作原理? ⭐⭐

答案:

Sentinel 实现主从自动故障转移:

监控: 每个 Sentinel 每秒向 Master/Slave/其他 Sentinel 发送 PING。

主观下线(SDOWN): 单个 Sentinel 认为 Master 不可达(超过 down-after-milliseconds)。

客观下线(ODOWN): 超过 quorum 个 Sentinel 都认为 Master 主观下线,则判定客观下线。

故障转移:

  1. Sentinel 之间选举 Leader(Raft)
  2. Leader 从 Slave 中选出新 Master(优先级 > 复制偏移量 > runid)
  3. 向新 Master 发送 SLAVEOF NO ONE
  4. 通知其他 Slave 复制新 Master
  5. 通知客户端新 Master 地址

Q9: Redis Cluster 的数据分片原理? ⭐⭐⭐

答案:

Redis Cluster 使用 哈希槽(Hash Slot) 分片,共 16384 个槽:

复制代码
slot = CRC16(key) % 16384

每个 Master 节点负责一部分槽(如 3 节点:0-5460 / 5461-10922 / 10923-16383)。

MOVED 重定向: 客户端请求到错误节点时,节点返回 MOVED slot ip:port,客户端重定向。

ASK 重定向: 槽迁移过程中,数据在源节点和目标节点都可能存在,用 ASK 临时重定向。

追问:为什么是 16384 个槽而不是更多?

  • 心跳包中携带槽位图,16384 个槽只需 2KB(16384/8 字节)
  • 节点数通常不超过 1000,16384 个槽已足够分配

Q10: Sentinel 和 Cluster 怎么选?

答案:

维度 Sentinel Cluster
数据量 单机可承载(< 几十 GB) 需要水平扩展(> 100 GB)
写吞吐 单 Master 限制 多 Master 并行写
运维复杂度 高(槽迁移、多节点管理)
多 key 操作 支持 受限(key 必须在同一槽)
客户端 简单 需要 Cluster 感知客户端

选型: 数据量 < 50GB 且写压力不大 → Sentinel;需要水平扩展 → Cluster。


四、缓存模式与经典问题

Q11: 缓存穿透、击穿、雪崩的区别和解决方案? ⭐⭐⭐

答案:

缓存穿透: 查询不存在的数据,缓存和数据库都没有,每次都打到数据库。

  • 解决:布隆过滤器(拦截不存在的 key);缓存空值(短 TTL)

缓存击穿: 热点 key 过期瞬间,大量请求同时打到数据库。

  • 解决:互斥锁(只有一个请求重建缓存);逻辑过期(不设 TTL,异步更新)

缓存雪崩: 大量 key 同时过期,或 Redis 宕机,请求全部打到数据库。

  • 解决:过期时间加随机抖动;Redis 高可用(Sentinel/Cluster);限流降级

Q12: 如何保证缓存和数据库的一致性? ⭐⭐⭐

答案:
常见方案对比:

方案 原理 问题
先更新 DB,再删缓存(推荐) 更新 DB 后删除缓存,下次读时重建 删缓存失败导致不一致
先删缓存,再更新 DB 删缓存后更新 DB 并发读在 DB 更新前重建旧缓存
延迟双删 更新 DB 后删缓存,延迟再删一次 延迟时间难以确定
Canal 订阅 binlog 监听 DB 变更,异步删缓存 架构复杂,有短暂不一致

推荐: 先更新 DB,再删缓存 + 删缓存失败重试(消息队列保证最终一致)。

Q13: 布隆过滤器的原理和误判率?

答案:

布隆过滤器用多个哈希函数将元素映射到 bit 数组的多个位置,全部置 1 表示存在。

查询: 所有对应位都为 1 → 可能存在;任意一位为 0 → 一定不存在。

特点:

  • 不存在的元素一定返回"不存在"(无漏报)
  • 存在的元素可能返回"不存在"(有误判,但概率可控)
  • 不支持删除(删除会影响其他元素)

误判率控制: 增大 bit 数组或增加哈希函数数量可降低误判率,但增加内存和计算开销。


五、分布式锁

Q14: 用 Redis 实现分布式锁的正确姿势? ⭐⭐⭐

答案:

复制代码
SET lock_key unique_value NX PX 30000
  • NX:不存在才设置(原子性)
  • PX 30000:30 秒自动过期(防止死锁)
  • unique_value:UUID,释放时验证是自己的锁

释放锁(Lua 脚本保证原子性):

lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

追问:为什么不能直接 DEL?

  • 可能删除别人的锁:锁过期后被其他线程获取,自己再 DEL 就删了别人的锁

Q15: Redlock 算法是什么?有什么争议? ⭐⭐

答案:

Redlock 在 N 个独立 Redis 节点上获取锁,超过 N/2+1 个成功才算获取成功。

步骤:

  1. 获取当前时间 t1
  2. 依次向 N 个节点请求加锁(超时时间远小于锁有效期)
  3. 计算获取锁耗时 t2-t1,锁实际有效期 = 设定有效期 - 耗时
  4. 成功节点数 > N/2+1 且有效期 > 0,则获取成功
  5. 失败则向所有节点释放锁

争议(Martin Kleppmann):

  • 时钟漂移可能导致锁提前过期
  • GC STW 期间锁可能过期,但进程认为仍持有锁
  • 对于强一致性场景,建议用 ZooKeeper 或 etcd

实践: 大多数场景单节点 Redis 锁足够,Redlock 适合对锁安全性要求极高的场景。


六、性能优化

Q16: 什么是 BigKey?如何处理? ⭐⭐

答案:

BigKey:单个 key 的 value 过大(String > 10KB,集合元素数 > 5000)。

危害:

  • 网络传输慢,阻塞其他命令(Redis 单线程)
  • 内存分配/释放耗时,导致延迟抖动
  • 集群模式下数据倾斜

发现:

bash 复制代码
redis-cli --bigkeys          # 扫描各类型最大 key
redis-cli --memkeys          # 按内存大小排序

处理:

  • String BigKey:拆分为多个 key(分片存储)
  • Hash/Set BigKey:按字段范围拆分为多个 key
  • 删除 BigKey:用 UNLINK(异步删除)代替 DEL(同步删除)

Q17: Pipeline 的原理和使用场景?

答案:

Pipeline 将多个命令批量发送,减少网络往返次数(RTT):

复制代码
普通:命令1 → 等待响应 → 命令2 → 等待响应 → ...(N次RTT)
Pipeline:命令1+2+...+N → 一次发送 → 批量响应(1次RTT)

注意:

  • Pipeline 不是原子的(中间可能有其他客户端命令插入)
  • 需要原子性用 Lua 脚本或 MULTI/EXEC 事务
  • 每批建议 100-1000 条命令,过多会占用大量内存

Q18: Redis 的慢查询如何排查?

答案:

bash 复制代码
# 配置慢查询阈值(微秒)
CONFIG SET slowlog-log-slower-than 10000  # 10ms

# 查看慢查询日志
SLOWLOG GET 10   # 最近 10 条
SLOWLOG LEN      # 慢查询总数
SLOWLOG RESET    # 清空

常见慢命令:

  • KEYS *:全量扫描,O(N),生产禁用,用 SCAN 替代
  • HGETALL:返回所有字段,Hash 很大时慢
  • SMEMBERS:返回所有成员,Set 很大时慢
  • SORT:排序操作,复杂度高

七、Redis 新特性

Q19: Redis 7.x 的重要新特性?

答案:

  • Redis Functions:替代 Lua 脚本,支持持久化和复制(脚本随数据一起复制)
  • Multi-Part AOF:AOF 拆分为多个文件,解决重写时的性能问题
  • ACL v2:更细粒度的权限控制(按 key 前缀、命令类别)
  • Listpack 全面替代 ziplist:更紧凑的内存编码

Q20: Redis Stream 是什么?和 List 做消息队列的区别?

答案:

Stream 是 Redis 5.0 引入的持久化消息流,支持消费者组:

维度 List Stream
消费模式 单消费者(LPOP) 消费者组(多消费者并行)
消息确认 无(LPOP 即删除) XACK 确认,未确认可重新消费
历史消息 消费后丢失 保留(可按 ID 范围查询)
消费者组 不支持 支持(多组独立消费)
持久化 支持 支持

Stream 适合: 需要消费确认、多消费者组、消息回溯的场景(轻量级 MQ)。


八、场景题

场景1: 电商秒杀系统如何用 Redis 防止超卖? ⭐⭐⭐

场景描述: 商品库存 100,瞬间 10 万并发请求,如何保证不超卖?

方案:

  1. 预热库存到 Redis :系统启动时 SET stock:1001 100
  2. 原子扣减(Lua 脚本)
lua 复制代码
local stock = tonumber(redis.call("get", KEYS[1]))
if stock <= 0 then return -1 end
redis.call("decrby", KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])
  1. 异步落库:扣减成功后发 MQ,消费者异步更新数据库
  2. 兜底校验 :数据库层加乐观锁(WHERE stock >= #{count}

关键点:

  • Lua 脚本保证"查询+扣减"原子性,避免 TOCTOU 竞态
  • Redis 单线程天然串行,不需要额外加锁
  • 库存预热时可用 SETNX 防止重复初始化

场景2: 如何用 Redis 实现接口限流(每用户每分钟 100 次)? ⭐⭐⭐

场景描述: API 网关需要对每个用户 ID 限制调用频率。

方案一:固定窗口(简单)

复制代码
key = "rate:uid:1001:202605111430"   # 分钟级 key
INCR key
EXPIRE key 60

缺点:窗口边界突刺(前一分钟末 + 后一分钟初可能 200 次)

方案二:滑动窗口(ZSet)

复制代码
key = "rate:uid:1001"
now = current_timestamp_ms
ZREMRANGEBYSCORE key 0 (now - 60000)   # 删除 1 分钟前的记录
count = ZCARD key
if count >= 100: reject
ZADD key now now
EXPIRE key 60

优点:精确滑动,无边界突刺

缺点:每次请求写入一条记录,内存占用较高

方案三:令牌桶(Redis + Lua)

  • 用 Hash 存储 {tokens, last_refill_time}
  • 每次请求用 Lua 脚本计算补充令牌数并消费

选型建议: 普通限流用固定窗口;精确限流用滑动窗口 ZSet;高并发令牌桶场景用 Lua 脚本。


场景3: 用户登录 Session 如何用 Redis 管理?支持单点登录踢出旧设备? ⭐⭐

场景描述: 用户登录后生成 Token,要求同一账号只能在一台设备登录,新登录踢出旧 Token。

设计:

复制代码
# 登录时
token = UUID()
SET session:{token} {user_info_json} EX 7200          # Token → 用户信息
SET user_token:{uid} {token} EX 7200                   # uid → 当前有效 Token

# 验证时
current_token = GET user_token:{uid}
if current_token != request_token: reject (已被踢出)
user_info = GET session:{current_token}

# 新设备登录时
old_token = GET user_token:{uid}
DEL session:{old_token}                                # 删除旧 Session
SET session:{new_token} ... / SET user_token:{uid} {new_token}

追问:如何支持多设备登录(最多 3 台)?

  • user_tokens:{uid} 存 ZSet,score 为登录时间
  • 登录时 ZADD,若 ZCARD > 3 则 ZRANGE 取最旧的踢出

场景4: 排行榜系统(实时积分榜)如何设计? ⭐⭐

场景描述: 游戏积分实时排行榜,支持查询 Top 100、查询某用户排名。

设计:

bash 复制代码
# 更新积分
ZADD leaderboard {score} {uid}

# 查 Top 100(降序)
ZREVRANGE leaderboard 0 99 WITHSCORES

# 查某用户排名(0-based,+1 得名次)
ZREVRANK leaderboard {uid}

# 查某用户积分
ZSCORE leaderboard {uid}

# 积分增量更新
ZINCRBY leaderboard {delta} {uid}

分页查询:

bash 复制代码
ZREVRANGE leaderboard {(page-1)*size} {page*size-1} WITHSCORES

追问:榜单数据量很大(百万用户)怎么优化?

  • 只维护 Top N(如 Top 10000)的 ZSet,超出范围的用数据库查
  • 定时任务清理低分用户:ZREMRANGEBYRANK leaderboard 0 -10001

场景5: 如何用 Redis 实现延迟队列(订单超时自动取消)? ⭐⭐⭐

场景描述: 用户下单后 30 分钟未支付自动取消,订单量大,如何高效实现?

设计(ZSet 延迟队列):

bash 复制代码
# 下单时,score = 当前时间 + 30分钟(Unix 时间戳)
ZADD delay_queue {now + 1800} {order_id}

# 消费者轮询(每秒执行)
now = current_timestamp
orders = ZRANGEBYSCORE delay_queue 0 {now} LIMIT 0 10
for order_id in orders:
    # 用 Lua 保证原子性:取出并删除
    ZREM delay_queue {order_id}
    # 处理超时逻辑

Lua 原子取出:

lua 复制代码
local items = redis.call("ZRANGEBYSCORE", KEYS[1], 0, ARGV[1], "LIMIT", 0, 10)
for _, v in ipairs(items) do
    redis.call("ZREM", KEYS[1], v)
end
return items

追问:和 RocketMQ 延迟消息相比?

  • Redis 延迟队列:实现简单,精度高(秒级),但不支持消费确认和重试
  • RocketMQ:支持消费确认、重试、死信队列,适合可靠性要求高的场景

场景6: 缓存预热方案设计(系统启动时如何避免冷启动打垮数据库)? ⭐⭐

场景描述: 服务重启后 Redis 缓存为空,大量请求直接打到数据库,如何处理?

方案:

  1. 启动时主动预热:服务启动时异步加载热点数据到 Redis(从数据库或离线统计的热点 key 列表)
  2. 分批预热:避免一次性加载导致数据库压力过大,分批次、限速加载
  3. 互斥锁防击穿 :预热期间缓存未就绪时,用 SETNX 互斥锁保证只有一个请求重建缓存
  4. 降级兜底:预热完成前,对非核心接口返回默认值或限流
java 复制代码
// 互斥锁重建缓存
String value = redis.get(key);
if (value == null) {
    if (redis.setnx("lock:" + key, "1", 10)) {  // 获取锁
        value = db.query(key);
        redis.set(key, value, ttl);
        redis.del("lock:" + key);
    } else {
        Thread.sleep(50);
        return get(key);  // 重试
    }
}

应用场景总结

场景 数据结构 关键命令/方案
分布式缓存 String SET NX PX,缓存穿透用布隆过滤器
排行榜 ZSet ZADD/ZRANGE/ZREVRANK
计数器/限流 String INCR,滑动窗口用 ZSet
分布式锁 String SET NX PX + Lua 释放
消息队列 Stream / List Stream 支持消费确认
好友关系 Set SINTER(共同好友)/SUNION
购物车 Hash HSET/HGETALL/HINCRBY
延迟队列 ZSet score 为执行时间,定时 ZRANGEBYSCORE
布隆过滤器 BitMap / RedisBloom BF.ADD/BF.EXISTS
地理位置 GEO GEOADD/GEODIST/GEORADIUS
相关推荐
sleP4o1 小时前
Redis 八股详解
redis
June bug1 小时前
【雅思学习笔记】口语Part1&3常见回答句型
职场和发展·学习方法
phltxy2 小时前
Redis Hash 数据类型:详解命令与实战场景
redis·算法·哈希算法
yuzhiboyouye10 小时前
web前端英语面试
前端·面试·状态模式
我是唐青枫10 小时前
终于不用手搓两级缓存了!C#.NET HybridCache 详解:L1 L2、标签失效与防击穿实战
redis·缓存·c#·.net
我叫黑大帅11 小时前
为什么需要 @types/react?解决“无法找到模块 react 的声明文件”报错
前端·javascript·面试
流年如夢13 小时前
栈和列队(LeetCode)
数据结构·算法·leetcode·链表·职场和发展
折哥的程序人生 · 物流技术专研13 小时前
Java面试85题图解版(一):基础核心篇
java·开发语言·后端·面试
Moment14 小时前
面试官:如果产品经理给你多个需求,怎么让AI去完成❓❓❓
前端·后端·面试