[特殊字符] Redis 热门八股问答 —— 面试高频题精选 覆盖基础原理、数据结构、持久化、缓存三大问题、分布式锁、高可用架构等核心考点每题附答案 追问链路 面试加分话术一、基础原理篇Q1Redis 为什么这么快答Redis 单线程能支撑 10W QPS核心原因有四纯内存操作数据全部存在内存中读写无磁盘 I/O持久化是异步的单线程模型避免了多线程的上下文切换和锁竞争开销I/O 多路复用基于 epoll/kqueue 实现单线程同时处理大量连接高效数据结构SDS、跳表、压缩列表等底层结构针对场景深度优化加分话术“Redis 6.0 引入了多线程 I/O处理网络读写但命令执行仍然是单线程所以不会出现并发安全问题。多线程只是用来解决网络 I/O 瓶颈。”追问为什么不用多线程执行命令多线程需要加锁引入锁竞争反而降低性能Redis 的瓶颈在网络 I/O 而非 CPU单线程足够单线程模型简单不会有死锁、上下文切换等问题Q2Redis 有哪些数据类型底层数据结构是什么答数据类型底层编码说明Stringint / embstr / raw (SDS)最常用可存字符串、整数、浮点数Listlistpack(quicklist)有序列表支持头尾操作Hashlistpack / hashtable哈希表适合存对象Setintset / hashtable无序集合自动去重ZSetlistpack / skiplisthashtable有序集合按 score 排序BitmapString 的二进制操作位图适合统计在线状态HyperLogLog概率算法基数统计误差 0.81%极省内存Streamradix tree listpool消息队列支持消费者组底层数据结构对照底层结构说明SDS (Simple Dynamic String)二进制安全的动态字符串O(1) 获取长度listpack紧凑列表替代 ziplistRedis 7.0quicklist双向链表 listpack 的混合结构skiplist跳表ZSet 的核心结构O(log N) 查找hashtable哈希表渐进式 rehashintset整数集合元素全为整数时使用加分话术“Redis 7.0 用 listpack 彻底替代了 ziplist解决了 ziplist 的级联更新问题。listpack 不保存前一个节点的长度所以修改一个节点不会导致连锁更新。”Q3String 类型的底层 SDS 和 C 字符串有什么区别答对比项C 字符串SDS获取长度O(n) 遍历O(1)维护 len 字段二进制安全❌ 遇 \0 截断✅ 以 len 判断结束缓冲区溢出可能不会自动扩容内存分配每次修改重新分配空间预分配 惰性释放减少重分配无扩容时多分配1MB 翻倍≥1MB 加 1MBQ4ZSet 为什么用跳表而不用红黑树答范围查询更高效跳表底层是有序链表找到起点后沿链表遍历即可红黑树需要中序遍历实现更简单跳表代码量远小于红黑树不易出 bug插入删除更灵活跳表只需修改指针红黑树需要旋转和重着色并发友好跳表可以局部加锁红黑树旋转影响范围大内存友好跳表可以通过调整层间距平衡空间和时间加分话术“跳表的查找时间复杂度也是 O(log N)和红黑树一样但跳表的常数因子更小缓存局部性更好。LevelDB 的 memtable 也用了跳表。”Q5Redis 的 hashtable 是怎么实现的什么时候扩容答Redis 的 hashtable 采用渐进式 rehash结构包含两个哈希表 ht[0] 和 ht[1]正常时只用 ht[0]触发扩容负载因子 1没有 BGSAVE 时负载因子 5BGSAVE 时避免 rehash 与子进程竞争渐进式 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 的分段迁移思想。”二、持久化篇Q6RDB 和 AOF 有什么区别答对比项RDBAOF原理定时生成内存快照二进制追加写命令日志文本触发方式save/bgsave/配置自动每次写/每秒/手动文件大小小压缩二进制大文本命令恢复速度快慢重放命令数据安全可能丢失最后一次快照后的数据最多丢 1 秒everysecfork 开销有bgsave 需要 fork 子进程无bgrewriteaof 时有适合场景冷备份、灾难恢复数据安全性要求高AOF 重写机制AOF 文件会不断增长需要定期重写压缩重写时 fork 子进程将当前内存数据转为命令写入新 AOF重写期间的新命令写入 AOF 重写缓冲区重写完成后追加加分话术“Redis 4.0 引入了混合持久化aof-use-rdb-preamble yesAOF 重写时先写 RDB 格式再追加增量 AOF兼顾了恢复速度和数据安全。”Q7RDB 的 bgsave 为什么不会阻塞主线程答bgsave调用时Redis 主进程执行fork()创建子进程fork()使用写时复制Copy-On-Write, COWfork 时父子进程共享同一份内存数据只有当主进程修改某块内存页时才会复制该页给子进程子进程负责将数据写入 RDB 文件主进程继续处理命令因此 bgsave 期间主进程基本不受影响只有 fork 瞬间和 COW 时有短暂开销追问COW 有什么风险如果 fork 之后有大量写操作会产生大量内存页复制导致内存占用翻倍生产环境建议maxmemory不要设置超过物理内存的 70%Q8AOF 的三种同步策略是什么答策略配置行为安全性性能alwaysappendfsync always每次写命令都同步磁盘最高不丢数据最差everysecappendfsync everysec每秒同步一次最多丢 1 秒推荐noappendfsync no由 OS 决定何时同步可能丢较多数据最好生产环境推荐everysec兼顾安全性和性能。三、缓存三大问题篇Q9缓存穿透是什么怎么解决答定义查询一个根本不存在的数据缓存永远不命中每次都打到数据库。通常是恶意攻击。解决方案方案原理优缺点缓存空值查不到也写入缓存设短 TTL如 30s简单但浪费内存布隆过滤器在缓存前加一层布隆过滤器拦截不存在的 key内存极小0.1% 误判率但不支持删除参数校验接口层校验非法参数如 id0基本防护布隆过滤器原理一个 bit 数组 多个哈希函数插入元素用 k 个哈希函数计算 k 个位置全部置 1查询元素k 个位置全为 1 → “可能存在”有 0 → “一定不存在”不支持删除因为可能影响其他元素加分话术“Redis 可以用 RedisBloom 模块实现布隆过滤器命令是 BF.ADD 和 BF.EXISTS。也可以用 Redisson 的 RBloomFilter。”// Redisson 布隆过滤器示例RBloomFilterStringfilterredisson.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){Stringvalueredis.get(key);if(valuenull){// 尝试获取分布式锁StringlockKeylock:key;if(redis.setnx(lockKey,1,30,TimeUnit.SECONDS)){try{valueredis.get(key);// 双重检查if(valuenull){valuedb.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实现最终一致性。这是目前生产环境最主流的方案。”四、分布式锁篇Q13Redis 分布式锁怎么实现答最基础的实现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 参数一条命令搞定天然原子。Q14Redisson 是怎么实现分布式锁的答Redisson 对 Redis 分布式锁做了大量增强特性实现方式可重入Hash 结构记录锁的持有者和重入次数自动续期Watch Dog后台线程每 10 秒检查锁是否还持有自动续期 30 秒阻塞等待通过 Redis 的 Pub/Sub 订阅锁释放事件避免忙轮询主从一致性RedLock 算法向 N 个独立 Redis 实例加锁多数成功才算成功RLocklockredisson.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 秒后锁自动释放Q15RedLock 算法是什么有什么争议答RedLock 步骤获取当前时间 T1依次向 N 个通常 5 个独立的 Redis 实例加锁设置较短超时获取当前时间 T2如果在 N/21 个以上实例加锁成功且总耗时 锁的 TTL → 加锁成功锁的有效时间 TTL - (T2 - T1)如果加锁失败向所有实例释放锁争议Martin Kleppmann《Designing Data-Intensive Applications》作者指出 RedLock 有安全问题依赖时钟同步如果某节点时钟跳跃可能导致锁提前过期GC 停顿可能导致客户端认为自己持有锁但实际上已过期AntirezRedis 作者回应可以通过 fencing token 解决加分话术“如果对一致性要求不是特别高单 Redis 实例 Redisson 的 Watch Dog 就够了。如果要求极高应该用 ZooKeeper 或 etcd 的临时节点方案。”五、高可用篇Q16Redis 主从复制是怎么工作的答全量同步首次连接 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 10Q17哨兵模式Sentinel是怎么工作的答Sentinel 负责监控、通知、自动故障转移监控每秒向主从节点发送 PING检测是否存活通知主节点下线时通过 Pub/Sub 通知客户端自动故障转移多个 Sentinel 投票确认主节点下线客观下线Sentinel 选举 leaderRaft 算法Leader 从从节点中选一个升级为主节点优先级 offset runid通知其他从节点切换主节点主观下线 vs 客观下线主观下线SDOWN单个 Sentinel 认为主节点不可达客观下线ODOWNquorum 个 Sentinel 都认为主节点不可达Q18Redis Cluster 分片集群是怎么工作的答核心原理16384 个哈希槽slot分布在多个节点上key 通过CRC16(key) % 16384计算属于哪个槽每个节点负责一部分槽节点 A: 0 ~ 5460 节点 B: 5461 ~ 10922 节点 C: 10923 ~ 16383客户端请求流程客户端发送命令到任意节点节点计算 key 的槽号如果在自己负责的槽 → 执行如果不在 → 返回 MOVED 重定向到正确节点ASK 重定向槽迁移期间槽从节点 A 迁移到节点 B 时A 返回 ASK客户端需先发 ASKING 到 B 再执行命令为什么是 16384 个槽作者 antirez 的解释心跳包中需要携带槽信息16384 个槽只需 2KBbitmap163840 个槽需要 20KB开销太大对于集群规模不超过 1000 节点的场景16384 个槽足够六、内存管理篇Q19Redis 的过期删除策略是什么答Redis 采用惰性删除 定期删除的组合策略策略原理优缺点惰性删除访问 key 时才检查是否过期对 CPU 友好但可能浪费内存定期删除每秒执行 10 次默认每次随机抽取 20 个 key 检查折中方案定期删除的流程每秒执行 10 次 1. 随机抽取 20 个设置了过期时间的 key 2. 删除其中已过期的 key 3. 如果过期比例 25%重复步骤 1 4. 每次执行时间不超过 25ms避免阻塞主线程追问如果大量 key 过期但没被访问会不会撑爆内存会。这就是为什么还需要内存淘汰策略。Q20Redis 的内存淘汰策略有哪些答Redis 4.0 有 8 种淘汰策略maxmemory-policy策略范围淘汰依据noeviction—不淘汰写入报错默认allkeys-random所有 key随机淘汰allkeys-lru所有 keyLRU最近最少使用allkeys-lfu所有 keyLFU最不经常使用volatile-random设了 TTL 的 key随机淘汰volatile-lru设了 TTL 的 keyLRUvolatile-lfu设了 TTL 的 keyLFUvolatile-ttl设了 TTL 的 keyTTL 越小越先淘汰推荐缓存场景用allkeys-lfuRedis 4.0它比 LRU 更准确——LRU 可能淘汰掉频繁访问但最近没访问的 keyLFU 综合考虑了访问频率和时间。Redis 的 LRU 是近似 LRU不是维护一个全局 LRU 链表太耗内存而是随机采样 5 个 keymaxmemory-samples淘汰其中最久未访问的采样数越大越精确但越慢七、实战场景篇Q21如何用 Redis 实现延迟队列答方案一ZSet 实现# 添加延迟消息score 为执行时间戳ZADD delay_queue1687000000task:1# 消费者轮询每秒一次ZRANGEBYSCORE delay_queue0当前时间戳LIMIT010# 取出后删除ZREM delay_queuetask:1方案二Redis StreamRedis 5.0# 生产者XADD tasks * namesend_emaildelay60000# 消费者组XGROUP CREATE tasks mygroup $ XREADGROUP GROUP mygroup consumer1 COUNT10BLOCK2000STREAMS tasks加分话术“如果对延迟精度要求高建议用 RocketMQ 的延迟队列或 Kafka 时间轮方案。Redis 方案适合对精度要求不高的场景。”Q22如何用 Redis 统计网站 UV独立访客答方案适用场景内存精度Set精确统计大每个用户一个元素精确Bitmap用户 ID 连续极小精确HyperLogLog大规模统计极小固定 12KB0.81% 误差# HyperLogLog推荐PFADD uv:20260618user:1001user:1002user: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:20260618Q23Redis 的大 key 怎么处理答什么是大 keyString 类型 value 10KBHash/List/Set/ZSet 元素数量 5000 个大 key 的危害读写耗时长阻塞其他请求内存不均衡Cluster 模式下某个节点压力大DEL 删除时阻塞主线程网络带宽占用大发现大 keyredis-cli--bigkeys# 扫描大 keyredis-cli--memkeys# 按内存排序MEMORY USAGEkey# 查看单个 key 的内存DEBUG OBJECTkey# 查看 key 的详细信息删除大 key避免阻塞# Redis 4.0异步删除UNLINKkey# 非阻塞删除# Hash分批删除HSCAN myhash0COUNT100HDEL myhash field1 field2...# List分批删除LTRIM mylist0-101# 每次保留前 100 个之外的# ZSet分批删除ZREMRANGEBYRANK myzset099# 每次删除 100 个Q24Redis 的 Pipeline 是什么为什么能提升性能答普通模式客户端 → 发送命令1 → 等待响应1 → 发送命令2 → 等待响应2 → ... 每次 RTTRound Trip Time都是一次网络往返Pipeline 模式客户端 → 批量发送命令1,2,3,...,N → 批量接收响应1,2,3,...,N 只需一次 RTT// Jedis Pipeline 示例Pipelinepipelinejedis.pipelined();for(inti0;i1000;i){pipeline.set(key:i,value:i);}pipeline.sync();// 一次性发送性能对比普通模式 10000 次 SET约 5 秒每次 0.5ms RTTPipeline 10000 次 SET约 0.1 秒一次 RTT注意Pipeline 不是原子操作中间可能插入其他客户端的命令。如果需要原子性用 Lua 脚本或事务。Q25Redis 事务和 Lua 脚本有什么区别答对比项MULTI/EXEC 事务Lua 脚本原子性命令排队一起执行但不支持回滚真正原子要么全执行要么全不执行事务隔离EXEC 前其他客户端命令可插入单线程执行天然隔离编程能力只能执行已有命令支持条件判断、循环等逻辑性能一般更好减少网络往返错误处理语法错误才中断运行时错误不回滚脚本错误全部中断# Redis 事务MULTI SET a1SET b2EXEC# Lua 脚本原子操作EVALredis.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/HyperLogLogZSet 底层跳表范围查询快、实现简单、并发友好RDB vs AOFRDB 快照恢复快AOF 日志丢数据少缓存穿透查询不存在的数据 → 布隆过滤器 / 缓存空值缓存击穿热点 key 过期 → 互斥锁 / 逻辑过期缓存雪崩大量 key 同时过期 → 随机 TTL / 多级缓存 / 限流双写一致性Cache Aside 延迟双删 / Canal MQ分布式锁SET NX PX Lua 释放 / Redisson Watch Dog过期策略惰性删除 定期删除内存淘汰allkeys-lfu推荐主从复制全量同步RDB 增量同步repl_backlog哨兵监控 自动故障转移Raft 选 leaderCluster16384 槽 CRC16 哈希 MOVED/ASK 重定向大 keyUNLINK 异步删除 HSCAN/LTRIM 分批删除Pipeline批量发送命令减少 RTT事务 vs LuaLua 更强真正原子 可编程