[特殊字符] Redis 热门八股问答 —— 面试高频题精选

[特殊字符] Redis 热门八股问答 —— 面试高频题精选

覆盖基础原理、数据结构、持久化、缓存三大问题、分布式锁、高可用架构等核心考点
每题附:答案 + 追问链路 + 面试加分话术


一、基础原理篇

Q1:Redis 为什么这么快?

答:Redis 单线程能支撑 10W+ QPS,核心原因有四:

  1. 纯内存操作:数据全部存在内存中,读写无磁盘 I/O(持久化是异步的)
  2. 单线程模型:避免了多线程的上下文切换和锁竞争开销
  3. I/O 多路复用:基于 epoll/kqueue 实现单线程同时处理大量连接
  4. 高效数据结构:SDS、跳表、压缩列表等底层结构针对场景深度优化

加分话术:“Redis 6.0 引入了多线程 I/O(处理网络读写),但命令执行仍然是单线程,所以不会出现并发安全问题。多线程只是用来解决网络 I/O 瓶颈。”

追问:为什么不用多线程执行命令?

  • 多线程需要加锁,引入锁竞争反而降低性能
  • Redis 的瓶颈在网络 I/O 而非 CPU,单线程足够
  • 单线程模型简单,不会有死锁、上下文切换等问题

Q2:Redis 有哪些数据类型?底层数据结构是什么?

答:

数据类型底层编码说明
Stringint / embstr / raw (SDS)最常用,可存字符串、整数、浮点数
Listlistpack(quicklist)有序列表,支持头尾操作
Hashlistpack / hashtable哈希表,适合存对象
Setintset / hashtable无序集合,自动去重
ZSetlistpack / skiplist+hashtable有序集合,按 score 排序
BitmapString 的二进制操作位图,适合统计在线状态
HyperLogLog概率算法基数统计,误差 0.81%,极省内存
Streamradix tree + listpool消息队列,支持消费者组

底层数据结构对照:

底层结构说明
SDS (Simple Dynamic String)二进制安全的动态字符串,O(1) 获取长度
listpack紧凑列表,替代 ziplist(Redis 7.0+)
quicklist双向链表 + listpack 的混合结构
skiplist跳表,ZSet 的核心结构,O(log N) 查找
hashtable哈希表,渐进式 rehash
intset整数集合,元素全为整数时使用

加分话术:“Redis 7.0 用 listpack 彻底替代了 ziplist,解决了 ziplist 的级联更新问题。listpack 不保存前一个节点的长度,所以修改一个节点不会导致连锁更新。”


Q3:String 类型的底层 SDS 和 C 字符串有什么区别?

答:

对比项C 字符串SDS
获取长度O(n) 遍历O(1)维护 len 字段
二进制安全❌ 遇 \0 截断✅ 以 len 判断结束
缓冲区溢出可能不会(自动扩容)
内存分配每次修改重新分配空间预分配 + 惰性释放
减少重分配扩容时多分配(<1MB 翻倍,≥1MB 加 1MB)

Q4:ZSet 为什么用跳表而不用红黑树?

答:

  1. 范围查询更高效:跳表底层是有序链表,找到起点后沿链表遍历即可;红黑树需要中序遍历
  2. 实现更简单:跳表代码量远小于红黑树,不易出 bug
  3. 插入删除更灵活:跳表只需修改指针,红黑树需要旋转和重着色
  4. 并发友好:跳表可以局部加锁,红黑树旋转影响范围大
  5. 内存友好:跳表可以通过调整层间距平衡空间和时间

加分话术:“跳表的查找时间复杂度也是 O(log N),和红黑树一样,但跳表的常数因子更小,缓存局部性更好。LevelDB 的 memtable 也用了跳表。”


Q5:Redis 的 hashtable 是怎么实现的?什么时候扩容?

答:

Redis 的 hashtable 采用渐进式 rehash

  1. 结构:包含两个哈希表 ht[0] 和 ht[1],正常时只用 ht[0]
  2. 触发扩容
    • 负载因子 > 1(没有 BGSAVE 时)
    • 负载因子 > 5(BGSAVE 时,避免 rehash 与子进程竞争)
  3. 渐进式 rehash
    • 为 ht[1] 分配空间(大小为第一个大于等于 ht[0].used×2 的 2^n)
    • 维护一个 rehashidx 索引,每次 CRUD 操作迁移 ht[0] 的一个桶到 ht[1]
    • 期间的新增操作直接写 ht[1],查找/删除先查 ht[0] 再查 ht[1]
    • 全部迁移完成后,释放 ht[0],ht[1] 变成 ht[0]

加分话术:“渐进式 rehash 避免了一次性 rehash 导致的服务卡顿,类似于 Java ConcurrentHashMap 的分段迁移思想。”


二、持久化篇

Q6:RDB 和 AOF 有什么区别?

答:

对比项RDBAOF
原理定时生成内存快照(二进制)追加写命令日志(文本)
触发方式save/bgsave/配置自动每次写/每秒/手动
文件大小(压缩二进制)大(文本命令)
恢复速度慢(重放命令)
数据安全可能丢失最后一次快照后的数据最多丢 1 秒(everysec)
fork 开销有(bgsave 需要 fork 子进程)无(bgrewriteaof 时有)
适合场景冷备份、灾难恢复数据安全性要求高

AOF 重写机制:

  • AOF 文件会不断增长,需要定期重写压缩
  • 重写时 fork 子进程,将当前内存数据转为命令写入新 AOF
  • 重写期间的新命令写入 AOF 重写缓冲区,重写完成后追加

加分话术:“Redis 4.0 引入了混合持久化(aof-use-rdb-preamble yes),AOF 重写时先写 RDB 格式再追加增量 AOF,兼顾了恢复速度和数据安全。”


Q7:RDB 的 bgsave 为什么不会阻塞主线程?

答:

  1. bgsave调用时,Redis 主进程执行fork()创建子进程
  2. fork()使用写时复制(Copy-On-Write, COW)
    • fork 时父子进程共享同一份内存数据
    • 只有当主进程修改某块内存页时,才会复制该页给子进程
  3. 子进程负责将数据写入 RDB 文件,主进程继续处理命令
  4. 因此 bgsave 期间主进程基本不受影响,只有 fork 瞬间和 COW 时有短暂开销

追问:COW 有什么风险?

  • 如果 fork 之后有大量写操作,会产生大量内存页复制,导致内存占用翻倍
  • 生产环境建议:maxmemory不要设置超过物理内存的 70%

Q8:AOF 的三种同步策略是什么?

答:

策略配置行为安全性性能
alwaysappendfsync always每次写命令都同步磁盘最高(不丢数据)最差
everysecappendfsync everysec每秒同步一次最多丢 1 秒推荐
noappendfsync no由 OS 决定何时同步可能丢较多数据最好

生产环境推荐everysec,兼顾安全性和性能。


三、缓存三大问题篇

Q9:缓存穿透是什么?怎么解决?

答:

定义:查询一个根本不存在的数据,缓存永远不命中,每次都打到数据库。通常是恶意攻击。

解决方案:

方案原理优缺点
缓存空值查不到也写入缓存(设短 TTL,如 30s)简单,但浪费内存
布隆过滤器在缓存前加一层布隆过滤器,拦截不存在的 key内存极小(0.1% 误判率),但不支持删除
参数校验接口层校验非法参数(如 id<0)基本防护

布隆过滤器原理:

  • 一个 bit 数组 + 多个哈希函数
  • 插入元素:用 k 个哈希函数计算 k 个位置,全部置 1
  • 查询元素:k 个位置全为 1 → “可能存在”;有 0 → “一定不存在”
  • 不支持删除(因为可能影响其他元素)

加分话术:“Redis 可以用 RedisBloom 模块实现布隆过滤器,命令是 BF.ADD 和 BF.EXISTS。也可以用 Redisson 的 RBloomFilter。”

// Redisson 布隆过滤器示例RBloomFilter<String>filter=redisson.getBloomFilter("userFilter");filter.tryInit(1000000L,0.01);// 预计100万元素,1%误判率filter.add("user:1001");if(!filter.contains("user:9999")){// 一定不存在,直接返回returnnull;}

Q10:缓存击穿是什么?怎么解决?

答:

定义:某个热点 key 过期的瞬间,大量并发请求同时打到数据库。

解决方案:

方案原理优缺点
互斥锁缓存 miss 时,用分布式锁保证只有一个线程查 DB 并回写缓存强一致,但性能差
逻辑过期key 不设物理 TTL,在 value 中存逻辑过期时间;发现过期时异步更新高性能,但短暂数据不一致
热点 key 永不过期不设 TTL,由后台定时刷新简单,但需要额外维护

互斥锁实现:

publicStringget(Stringkey){Stringvalue=redis.get(key);if(value==null){// 尝试获取分布式锁StringlockKey="lock:"+key;if(redis.setnx(lockKey,"1",30,TimeUnit.SECONDS)){try{value=redis.get(key);// 双重检查if(value==null){value=db.query(key);redis.set(key,value,60,TimeUnit.SECONDS);}}finally{redis.del(lockKey);}}else{// 未获取到锁,短暂等待后重试Thread.sleep(50);returnget(key);}}returnvalue;}

Q11:缓存雪崩是什么?怎么解决?

答:

定义:大量 key 同时过期Redis 宕机,导致请求全部打到数据库。

与穿透/击穿的区别:

问题触发条件影响范围
穿透查询不存在的数据单个 key
击穿热点 key过期单个热点 key
雪崩大量 key同时过期 / Redis 宕机大面积

解决方案:

方案原理
随机过期时间TTL 加随机值,避免集中过期(如 base + random(0, 300))
多级缓存本地缓存(Caffeine) + Redis + DB,层层拦截
限流降级对数据库加限流,超过阈值直接返回默认值
集群高可用Redis Sentinel / Cluster,避免单点故障
熔断机制数据库压力过大时触发熔断,返回兜底数据

Q12:缓存和数据库的双写一致性怎么保证?

答:

这是经典的分布式一致性问题,没有完美方案,只有权衡。

四种策略:

策略操作顺序一致性性能
Cache Aside先删缓存 → 更新 DB最终一致
Read/Write Through缓存层统一代理读写强一致
Write Behind只更新缓存,异步刷 DB最终一致最好
延迟双删先删缓存 → 更新 DB → sleep → 再删缓存最终一致

Cache Aside 模式(最常用):

读:先读缓存 → miss 则读 DB → 回写缓存 写:先删缓存 → 再更新 DB

延迟双删解决并发问题:

// 1. 先删缓存redis.del(key);// 2. 更新数据库db.update(key,newValue);// 3. 延迟一段时间(略大于一次读请求的耗时)Thread.sleep(500);// 4. 再删一次缓存(删掉并发读请求写入的旧值)redis.del(key);

追问:延迟双删的延迟时间怎么确定?

  • 略大于一次读请求的总耗时(网络 + DB 查询 + 缓存写入)
  • 一般 300ms ~ 1s,可以通过监控读请求 P99 耗时来确定

加分话术:“如果对一致性要求很高,可以用 Canal 监听 MySQL binlog,通过 MQ 异步更新 Redis,实现最终一致性。这是目前生产环境最主流的方案。”


四、分布式锁篇

Q13:Redis 分布式锁怎么实现?

答:

最基础的实现:SETNX + EXPIRE

# 原子操作:设置 key + 过期时间SET lock_key unique_value NX PX30000# NX: 不存在才设置# PX: 过期时间(毫秒)

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

ifredis.call("get",KEYS[1])==ARGV[1]thenreturnredis.call("del",KEYS[1])elsereturn0end

为什么不直接用 DEL?因为要校验 value 是否是自己设置的,防止误删别人的锁。

追问:SETNX 和 EXPIRE 不是两条命令吗?会不会不原子?

  • Redis 2.6.12 之后,SET 命令支持 NX + PX 参数,一条命令搞定,天然原子。

Q14:Redisson 是怎么实现分布式锁的?

答:

Redisson 对 Redis 分布式锁做了大量增强:

特性实现方式
可重入Hash 结构记录锁的持有者和重入次数
自动续期(Watch Dog)后台线程每 10 秒检查锁是否还持有,自动续期 30 秒
阻塞等待通过 Redis 的 Pub/Sub 订阅锁释放事件,避免忙轮询
主从一致性RedLock 算法(向 N 个独立 Redis 实例加锁,多数成功才算成功)
RLocklock=redisson.getLock("myLock");try{// 尝试加锁,最多等待 10 秒,自动续期if(lock.tryLock(10,-1,TimeUnit.SECONDS)){// 业务逻辑}}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}

Watch Dog 机制详解:

  • tryLock()传 -1 时,不指定 leaseTime,启用 Watch Dog
  • 默认 lockWatchdogTimeout = 30 秒
  • 每 10 秒(lockWatchdogTimeout / 3)续期一次
  • 如果持有锁的客户端宕机,Watch Dog 停止,30 秒后锁自动释放

Q15:RedLock 算法是什么?有什么争议?

答:

RedLock 步骤:

  1. 获取当前时间 T1
  2. 依次向 N 个(通常 5 个)独立的 Redis 实例加锁,设置较短超时
  3. 获取当前时间 T2,如果在 N/2+1 个以上实例加锁成功,且总耗时 < 锁的 TTL → 加锁成功
  4. 锁的有效时间 = TTL - (T2 - T1)
  5. 如果加锁失败,向所有实例释放锁

争议:

  • Martin Kleppmann(《Designing Data-Intensive Applications》作者)指出 RedLock 有安全问题
    • 依赖时钟同步,如果某节点时钟跳跃,可能导致锁提前过期
    • GC 停顿可能导致客户端认为自己持有锁,但实际上已过期
  • Antirez(Redis 作者)回应:可以通过 fencing token 解决

加分话术:“如果对一致性要求不是特别高,单 Redis 实例 + Redisson 的 Watch Dog 就够了。如果要求极高,应该用 ZooKeeper 或 etcd 的临时节点方案。”


五、高可用篇

Q16:Redis 主从复制是怎么工作的?

答:

全量同步(首次连接): 1. 从节点发送 PSYNC ? -1 2. 主节点执行 BGSAVE 生成 RDB,发送给从节点 3. 从节点加载 RDB 4. 主节点将期间的写命令发送给从节点 增量同步(断线重连): 1. 从节点发送 PSYNC replid offset 2. 主节点检查 replid 是否一致,offset 是否在 repl_backlog 内 3. 如果在,发送 offset 之后的增量数据 4. 如果不在,退化为全量同步

关键配置:

# 主节点:至少 N 个从节点在线才接受写入 min-replicas-to-write 1 min-replicas-max-lag 10

Q17:哨兵模式(Sentinel)是怎么工作的?

答:

Sentinel 负责监控、通知、自动故障转移:

  1. 监控:每秒向主从节点发送 PING,检测是否存活
  2. 通知:主节点下线时,通过 Pub/Sub 通知客户端
  3. 自动故障转移
    • 多个 Sentinel 投票确认主节点下线(客观下线)
    • Sentinel 选举 leader(Raft 算法)
    • Leader 从从节点中选一个升级为主节点(优先级 > offset > runid)
    • 通知其他从节点切换主节点

主观下线 vs 客观下线:

  • 主观下线(SDOWN):单个 Sentinel 认为主节点不可达
  • 客观下线(ODOWN):quorum 个 Sentinel 都认为主节点不可达

Q18:Redis Cluster 分片集群是怎么工作的?

答:

核心原理:

  • 16384 个哈希槽(slot),分布在多个节点上
  • key 通过CRC16(key) % 16384计算属于哪个槽
  • 每个节点负责一部分槽
节点 A: 0 ~ 5460 节点 B: 5461 ~ 10922 节点 C: 10923 ~ 16383

客户端请求流程:

  1. 客户端发送命令到任意节点
  2. 节点计算 key 的槽号
  3. 如果在自己负责的槽 → 执行
  4. 如果不在 → 返回 MOVED 重定向到正确节点

ASK 重定向(槽迁移期间):

  • 槽从节点 A 迁移到节点 B 时,A 返回 ASK,客户端需先发 ASKING 到 B 再执行命令

为什么是 16384 个槽?

  • 作者 antirez 的解释:心跳包中需要携带槽信息,16384 个槽只需 2KB(bitmap),163840 个槽需要 20KB,开销太大
  • 对于集群规模不超过 1000 节点的场景,16384 个槽足够

六、内存管理篇

Q19:Redis 的过期删除策略是什么?

答:

Redis 采用惰性删除 + 定期删除的组合策略:

策略原理优缺点
惰性删除访问 key 时才检查是否过期对 CPU 友好,但可能浪费内存
定期删除每秒执行 10 次(默认),每次随机抽取 20 个 key 检查折中方案

定期删除的流程:

每秒执行 10 次: 1. 随机抽取 20 个设置了过期时间的 key 2. 删除其中已过期的 key 3. 如果过期比例 > 25%,重复步骤 1 4. 每次执行时间不超过 25ms(避免阻塞主线程)

追问:如果大量 key 过期但没被访问,会不会撑爆内存?
会。这就是为什么还需要内存淘汰策略


Q20:Redis 的内存淘汰策略有哪些?

答:

Redis 4.0+ 有 8 种淘汰策略(maxmemory-policy):

策略范围淘汰依据
noeviction不淘汰,写入报错(默认)
allkeys-random所有 key随机淘汰
allkeys-lru所有 keyLRU(最近最少使用)
allkeys-lfu所有 keyLFU(最不经常使用)
volatile-random设了 TTL 的 key随机淘汰
volatile-lru设了 TTL 的 keyLRU
volatile-lfu设了 TTL 的 keyLFU
volatile-ttl设了 TTL 的 keyTTL 越小越先淘汰

推荐:缓存场景用allkeys-lfu(Redis 4.0+),它比 LRU 更准确——LRU 可能淘汰掉频繁访问但最近没访问的 key,LFU 综合考虑了访问频率和时间。

Redis 的 LRU 是近似 LRU:

  • 不是维护一个全局 LRU 链表(太耗内存)
  • 而是随机采样 5 个 key(maxmemory-samples),淘汰其中最久未访问的
  • 采样数越大越精确,但越慢

七、实战场景篇

Q21:如何用 Redis 实现延迟队列?

答:

方案一:ZSet 实现

# 添加延迟消息,score 为执行时间戳ZADD delay_queue1687000000"task:1"# 消费者轮询(每秒一次)ZRANGEBYSCORE delay_queue0<当前时间戳>LIMIT010# 取出后删除ZREM delay_queue"task:1"

方案二:Redis Stream(Redis 5.0+)

# 生产者XADD tasks * name"send_email"delay60000# 消费者组XGROUP CREATE tasks mygroup $ XREADGROUP GROUP mygroup consumer1 COUNT10BLOCK2000STREAMS tasks>

加分话术:“如果对延迟精度要求高,建议用 RocketMQ 的延迟队列或 Kafka + 时间轮方案。Redis 方案适合对精度要求不高的场景。”


Q22:如何用 Redis 统计网站 UV(独立访客)?

答:

方案适用场景内存精度
Set精确统计大(每个用户一个元素)精确
Bitmap用户 ID 连续极小精确
HyperLogLog大规模统计极小(固定 12KB)0.81% 误差
# HyperLogLog(推荐)PFADD uv:20260618"user:1001""user:1002""user:1001"# 自动去重PFCOUNT uv:20260618# 返回 2# Bitmap(用户 ID 为整数时)SETBIT uv:2026061810011# 用户 1001 访问SETBIT uv:2026061810021# 用户 1002 访问BITCOUNT uv:20260618# 返回 2# 多天合并BITOP OR uv:week uv:20260616 uv:20260617 uv:20260618

Q23:Redis 的大 key 怎么处理?

答:

什么是大 key?

  • String 类型 value > 10KB
  • Hash/List/Set/ZSet 元素数量 > 5000 个

大 key 的危害:

  • 读写耗时长,阻塞其他请求
  • 内存不均衡(Cluster 模式下某个节点压力大)
  • DEL 删除时阻塞主线程
  • 网络带宽占用大

发现大 key:

redis-cli--bigkeys# 扫描大 keyredis-cli--memkeys# 按内存排序MEMORY USAGE<key># 查看单个 key 的内存DEBUG OBJECT<key># 查看 key 的详细信息

删除大 key(避免阻塞):

# Redis 4.0+:异步删除UNLINK<key># 非阻塞删除# Hash:分批删除HSCAN myhash0COUNT100HDEL myhash field1 field2...# List:分批删除LTRIM mylist0-101# 每次保留前 100 个之外的# ZSet:分批删除ZREMRANGEBYRANK myzset099# 每次删除 100 个

Q24:Redis 的 Pipeline 是什么?为什么能提升性能?

答:

普通模式:

客户端 → 发送命令1 → 等待响应1 → 发送命令2 → 等待响应2 → ... 每次 RTT(Round Trip Time)都是一次网络往返

Pipeline 模式:

客户端 → 批量发送命令1,2,3,...,N → 批量接收响应1,2,3,...,N 只需一次 RTT
// Jedis Pipeline 示例Pipelinepipeline=jedis.pipelined();for(inti=0;i<1000;i++){pipeline.set("key:"+i,"value:"+i);}pipeline.sync();// 一次性发送

性能对比:

  • 普通模式 10000 次 SET:约 5 秒(每次 0.5ms RTT)
  • Pipeline 10000 次 SET:约 0.1 秒(一次 RTT)

注意:Pipeline 不是原子操作,中间可能插入其他客户端的命令。如果需要原子性,用 Lua 脚本或事务。


Q25:Redis 事务和 Lua 脚本有什么区别?

答:

对比项MULTI/EXEC 事务Lua 脚本
原子性命令排队一起执行,但不支持回滚真正原子,要么全执行要么全不执行
事务隔离EXEC 前其他客户端命令可插入单线程执行,天然隔离
编程能力只能执行已有命令支持条件判断、循环等逻辑
性能一般更好(减少网络往返)
错误处理语法错误才中断,运行时错误不回滚脚本错误全部中断
# Redis 事务MULTI SET a1SET b2EXEC# Lua 脚本(原子操作)EVAL"redis.call('SET', KEYS[1], ARGV[1]); redis.call('SET', KEYS[2], ARGV[2])"2a b12

加分话术:“Redis 事务的 MULTI/EXEC 不支持回滚,这是设计决策——Redis 认为事务失败应该由应用层处理,而不是数据库回滚。而且运行时错误(如对 String 执行 LPUSH)不应该影响其他正确命令的执行。”


八、高频追问链路

面试官追问路线图

Q: Redis 为什么快? → Q: 单线程为什么不用多线程? → Q: Redis 6.0 的多线程是什么? → Q: I/O 多路复用的原理? Q: 缓存穿透怎么解决? → Q: 布隆过滤器原理? → Q: 布隆过滤器误判率怎么控制? → Q: 布隆过滤器支持删除吗?用什么替代? Q: 缓存击穿怎么解决? → Q: 分布式锁怎么实现? → Q: Redisson 的 Watch Dog 机制? → Q: RedLock 算法有争议你怎么看? Q: Redis 持久化? → Q: RDB 的 COW 有什么风险? → Q: AOF 重写的原理? → Q: 混合持久化怎么配置? Q: Redis Cluster 怎么分片? → Q: 为什么是 16384 个槽? → Q: MOVED 和 ASK 重定向的区别? → Q: 槽迁移过程中数据会丢吗?

📋 Redis 面试速查表

考点一句话回答
为什么快内存 + 单线程 + I/O 多路复用 + 高效数据结构
数据类型String/List/Hash/Set/ZSet + Stream/Bitmap/HyperLogLog
ZSet 底层跳表(范围查询快、实现简单、并发友好)
RDB vs AOFRDB 快照恢复快,AOF 日志丢数据少
缓存穿透查询不存在的数据 → 布隆过滤器 / 缓存空值
缓存击穿热点 key 过期 → 互斥锁 / 逻辑过期
缓存雪崩大量 key 同时过期 → 随机 TTL / 多级缓存 / 限流
双写一致性Cache Aside + 延迟双删 / Canal + MQ
分布式锁SET NX PX + Lua 释放 / Redisson + Watch Dog
过期策略惰性删除 + 定期删除
内存淘汰allkeys-lfu(推荐)
主从复制全量同步(RDB) + 增量同步(repl_backlog)
哨兵监控 + 自动故障转移(Raft 选 leader)
Cluster16384 槽 + CRC16 哈希 + MOVED/ASK 重定向
大 keyUNLINK 异步删除 + HSCAN/LTRIM 分批删除
Pipeline批量发送命令,减少 RTT
事务 vs LuaLua 更强(真正原子 + 可编程)