面试官接连追问 Redis,我用项目实战扛了 40 分钟
2026/6/10 8:34:40 网站建设 项目流程


前言

最近跑了几场 Java 后端实习面试,发现一个规律:Redis 是面试官最爱顺着问的技术栈,几乎每一面都会被问到,而且不是问一两个点就停——数据结构和底层实现来回切,项目用法和八股原理交叉问。

这篇文章把我被问到的 Redis 高频题整理出来,不是单纯罗列八股,而是每个知识点都绑一个具体的项目使用场景——面试官问你怎么用的,你能从场景讲到原理再讲回代码。

读完你会收获:

  • Redis 面试被问的五个核心方向,每个方向怎么结构化回答
  • 项目里的秒杀、缓存、Feed 流、GEO 分别对应哪些八股考点
  • 几个面试官最爱追问的"对比题"(Redisson vs SETNX、Stream vs RabbitMQ、推模式 vs 拉模式)怎么答出深度
  • 从第一个问题一路扛到第 6 层追问的完整话术思路

一、Redis 是什么?别只背"内存数据库"

面试官问:讲讲你对 Redis 的理解。

别这样答:“Redis 是一个键值对内存数据库,读写速度快,一般用来做缓存。” 这个回答面试官听了一百遍了。

建议这样分层讲

第一层,数据结构。Redis 不只是 KV——String、List、Hash、Set、ZSet 五种基本类型之外,还有 Bitmap、GEO、HyperLogLog、Stream 这些扩展类型。每种数据结构背后都有对应的业务场景。

第二层,性能。单线程 + epoll IO 多路复用 + 内存操作。单线程不是说只能处理一个连接——epoll 让它在同一时刻能监听成千上万个 socket,只是执行命令的时候是串行的。这就是为什么 Lua 脚本能保证原子性——执行期间不会穿插其他命令。

第三层,工程角色。Redis 在实际项目里承担的角色比缓存多得多。我在 O2O 项目里用到了:

场景用的 Redis 什么效果
秒杀库存扣减Lua 脚本原子校验+扣减+投递,1000+ QPS 零超卖
秒杀削峰Stream + Consumer Group异步落库,消费失败 Pending 重试
订单超时释放Redisson 延迟队列15 分钟未支付自动取消回补库存
Feed 流推送ZSet 收件箱时间游标分页,毫秒级拉取
附近商户GEO + GEOSEARCH5km 半径毫秒级返回
限流防刷ZSet 滑动窗口四维度限流
分布式锁Redisson + WatchDogDB 层二次去重

💡 面试技巧:这样回答,面试官会觉得你不是在背八股,是真的用 Redis 解决过实际问题。而且你主动抛出了 Lua、Stream、Redisson、GEO 这些关键词——面试官大概率会顺着其中一个往下问,而你已经准备好了。这就是好的自我介绍/项目介绍的核心逻辑:你不是在回答问题,你是在引导面试官往你准备好的方向问。



二、数据结构底层——面试官最爱追问的一集

面试官问:Redis 的 ZSet 底层是什么?

你要答

ZSet 底层是skiplist(跳表)+ dict(哈希表)的组合。dict 用来 O(1) 按 member 查 score,skiplist 用来 O(log n) 做范围查询和排序。

跳表原理(面试高频)

跳表就是多层有序链表。最底层是一个完整的有序双向链表,每往上一层节点数约减半——通过随机函数决定一个节点是否"晋升"到上一层。

查找的时候从最高层开始,类似二分查找:下一个节点 score 大于目标就降一层,直到找到或者确认不存在。



为什么用跳表不用红黑树?

  1. 跳表实现比红黑树简单——旋转和变色真的容易写出 bug
  2. 跳表天然支持范围查询:底层是双向链表,找到起点后顺着往下扫就行
  3. 红黑树范围查询需要中序遍历,麻烦一些

其他数据结构的底层简表

数据结构底层实现
Stringint / embstr(≤44字节)/ raw
Listquicklist(linkedlist + ziplist 混合)
Hashlistpack(7.0+)/ ziplist → 数据多转 hashtable
Setintset(全整数+少元素)→ 转 hashtable
ZSetlistpack(7.0+)→ 转 skiplist + dict

面试官追问:跳表和 B+ Tree 的区别?

跳表是 Redis 用的(内存、随机访问快),B+ Tree 是 MySQL 用的(磁盘 IO、顺序访问优化)。B+ Tree 非叶子节点不存数据,树更矮,磁盘 IO 更少。跳表节点随机分布,适合内存场景。两者都是 O(log n),但优化的方向不同——B+ Tree 优化磁盘寻道,跳表优化内存查找。


三、缓存三防——面试必考题,搞错顺序很致命

面试官问:缓存穿透、击穿、雪崩分别是什么?怎么解决?

第一步:先把概念说清楚

问题现象根因
穿透查一个根本不存在的数据,缓存里没有,每次都打到 DB恶意攻击 / 查不存在的 Key
击穿一个热点 Key 过期,瞬间大量请求同时打到 DB热点 Key 过期 + 高并发
雪崩大量 Key 同时过期 / Redis 挂了,DB 瞬间被打爆缓存集体失效


第二步:每个问题对应的解决方案

防穿透

// 方案一:布隆过滤器 —— 先问"这个 Key 可能存在吗"if(!bloomFilter.mightExist(key)){returnnull;// 一定不存在,直接返回,不用查 DB}// 方案二:空值缓存 —— 查了 DB 发现不存在,也缓存一个短 TTL 的空值if(data==null){redis.set(key,"",2,TimeUnit.MINUTES);// 2分钟后过期returnnull;}

防击穿(最爱追问:请求合并 vs 互斥锁)

// 请求合并:只放一个请求去查 DB,其余的等结果共享privateConcurrentHashMap<String,CompletableFuture<String>>flyMap=newConcurrentHashMap<>();publicStringget(Stringkey){Stringcached=redis.get(key);if(cached!=null)returncached;// 同一个 key 只有一个 "飞行中" 的请求CompletableFuture<String>future=flyMap.computeIfAbsent(key,k->CompletableFuture.supplyAsync(()->{StringdbData=db.query(key);redis.set(key,dbData,30,TimeUnit.MINUTES);returndbData;}).whenComplete((r,e)->flyMap.remove(key)));returnfuture.get(3,TimeUnit.SECONDS);// 所有等待者共享同一个 Future}

请求合并 vs SETNX 的本质区别

  • SETNX 互斥锁:排队执行——拿到锁的去查 DB,其他人等锁释放后自己去读缓存
  • 请求合并:结果共享——拿到锁的去查 DB,其他人直接拿同一个结果,不用自己再查

这个区别面试官特别爱追问,因为它能看出你是真的理解了还是只是背了方案名。关键点在于是"各自重建"还是"共享结果"。

防雪崩

// TTL 加随机抖动,避免集体过期intttl=30*60+ThreadLocalRandom.current().nextInt(300);// 30分钟 + 0~5分钟随机redis.set(key,value,ttl,TimeUnit.SECONDS);// 逻辑过期:value 里带一个过期时间戳// 过期后异步重建,读请求先返回旧值——不阻塞用户

四、持久化——RDB vs AOF,别只说"两个都开"

面试官问:Redis 怎么保证数据不丢?

你要答

两套机制,各有侧重:

RDBAOF
原理快照:某个时间点全量内存数据写磁盘日志:每条写命令追加到文件
文件大小小(压缩二进制)大(文本命令)
恢复速度快(直接加载二进制)慢(逐条重放)
数据安全可能丢最后一次快照后的数据everysec最多丢 1 秒
fork 影响fork 子进程瞬间阻塞(页表复制)持续 IO,轻微影响

生产推荐:Redis 4.0+ 用混合持久化——AOF 文件前半段是 RDB 快照,后半段是增量 AOF。兼顾恢复速度和数据安全。

面试官追问:RDB fork 子进程为什么有瞬间阻塞?

fork()系统调用会复制父进程的页表(不是复制整个内存——写时复制)。10GB 的 Redis 进程,页表可能有几十 MB,复制需要时间。期间主进程阻塞。所以bgsave虽然叫"后台保存",但不是完全没有影响。


五、过期删除和内存淘汰——别搞混了

面试官问:Redis 的 Key 过期了怎么删?内存满了怎么办?

这是两件事,千万别搞混

过期删除策略(Key 设置了 TTL,到期了怎么删)

  1. 惰性删除:访问 Key 的时候顺便检查有没有过期 → 过期了删掉。优点是对 CPU 友好,缺点是有过期 Key 一直没人访问就占着内存。
  2. 定期删除:每 100ms 随机抽一批 Key 检查,过期就删,每次执行不超过 25ms。
  3. 两者结合:定期清理大部分,惰性兜底。

内存淘汰策略(内存满了,新写入怎么办,共 8 种)

策略行为
noeviction不淘汰,写入直接报错(默认策略)
allkeys-lru所有 Key 中淘汰最久未使用的
allkeys-lfu(4.0+)所有 Key 中淘汰最少使用频率的
allkeys-random所有 Key 中随机淘汰
volatile-lru只在设了过期时间的 Key 中淘汰最久未使用的
volatile-lfu(4.0+)只在设了过期时间的 Key 中淘汰最少使用的
volatile-random只在设了过期时间的 Key 中随机淘汰
volatile-ttl淘汰 TTL 最短(最早过期)的

LRU vs LFU:LRU 看"最近一次什么时候用的"——适合热点有明显时间特征的场景。LFU 看"一共用了多少次"——适合有些数据虽然最近用过但整体不频繁的场景。

一个易错点:Redis 的 LRU 是近似算法,随机抽 5 个 Key 淘汰其中最不合适的,不是全局遍历。面试官问到这一层,你说出"近似"两个字就已经领先了。


六、秒杀场景的 Redis 深度用法——面试官顺着问的根本停不下来

这一节是我被问得最多的。O2O 项目里秒杀链路重度依赖 Redis,面试官从"Lua 为什么原子性"一路问到"Redis 和 MySQL 没有事务怎么兜底"。


整体链路

用户请求 │ ▼ ┌─ Redis Lua 脚本 ──────────────────┐ │ ① 校验库存(GET stock) │ │ ② 一人一单去重(SISMEMBER) │ │ ③ 库存不足 → 候补入队(ZADD) │ │ ④ 扣库存(DECR) │ │ ⑤ 标记已购(SADD) │ │ ⑥ 投递订单消息(XADD Stream) │ │ 全部原子执行,不可打断 │ └────────────────────────────────────┘ │ ▼ Redis Stream + Consumer Group → 异步落库 │ ▼ Redisson 分布式锁 → DB 层二次去重 │ ▼ MySQL 条件更新 → 乐观锁兜底

Lua 脚本核心逻辑

-- KEYS[1]: 库存 key KEYS[2]: 已购集合 KEYS[3]: 候补队列 KEYS[4]: Stream-- ARGV[1]: 用户ID ARGV[2]: 订单ID ARGV[3]: 秒杀券ID-- 1. 校验库存localstock=tonumber(redis.call('GET',KEYS[1]))ifstock==nilorstock<=0thenredis.call('ZADD',KEYS[3],redis.call('TIME')[1],ARGV[1]..':'..ARGV[2])return{-1,'no_stock_and_queued'}end-- 2. 一人一单ifredis.call('SISMEMBER',KEYS[2],ARGV[1])==1thenreturn{0,'already_bought'}end-- 3. 扣库存 + 标记 + 投递redis.call('DECR',KEYS[1])redis.call('SADD',KEYS[2],ARGV[1])redis.call('XADD',KEYS[4],'*','userId',ARGV[1],'orderId',ARGV[2])return{1,'success'}

几个关键设计点

  • 校验放在最前面:不可逆操作(扣库存、标记已购)放最后,脚本跑到一半 Redis 挂了也不会有脏数据
  • DECR 一条命令完成扣减:不需要先 GET 再 SET,本身就是原子的
  • Stream 投递在脚本内:保证"扣库存成功 = 消息一定投递了"

面试官追问:Redis 扣了库存但 MySQL 写失败了怎么办?

四层兜底

  1. Lua 脚本先把关:前置拦截,校验不通过根本不扣
  2. Redisson 分布式锁做 DB 层二次去重:同一用户同一券只放一个进 DB
  3. MySQL 条件更新兜底UPDATE ... SET stock = stock - 1 WHERE stock > 0,affected_rows=0 就不扣
  4. 定时对账:异步比对 Redis 和 MySQL 库存差异,回补

为什么不用 Seata 分布式事务?秒杀场景 QPS 决定了不能用强一致性方案。Seata AT 模式的全局锁会成为瓶颈。而且秒杀业务允许"少卖"(Redis 多扣了回补),不允许"超卖"——库存方向一致性要求是单向的,最终一致就够了。


七、分布式锁——Redisson vs SETNX

面试官问:为什么用 Redisson 而不是自己用 SETNX 实现?

SETNX 手写Redisson
锁过期手动设 TTL,业务跑久了锁自动释放WatchDog 自动续期(每 10 秒续到 30 秒)
解锁安全直接 DEL 可能误删别人的锁Lua 脚本校验持有者再删
可重入要自己实现计数器内置,同一线程可多次获取
其他能力读写锁、信号量、闭锁全套 JUC 等价实现

核心区别是 WatchDog:拿到锁后启动后台定时任务自动续期,只要 JVM 不挂锁就不过期。SETNX 设 10 秒 TTL——万一 GC 停顿或业务慢了一点,锁在第 10 秒自动释放,另一个请求拿到锁,并发问题就来了。

面试官追问:Redisson 延迟队列底层用什么实现的?

Redis 的 PUB/SUB + ZSet 混合方案。ZSet 做延迟排序(score 存到期时间戳)→ 定时轮询 ZSet 扫描到期元素 → 通过 PUB/SUB 通知订阅者。延迟精度秒级,适合订单超时释放(15 分钟级)这种场景。要毫秒级精度得用 RabbitMQ 延迟交换机。


八、Redis Stream vs RabbitMQ——项目中为什么两个都用?

面试官问:同一套系统里同时用了 Redis Stream 和 RabbitMQ——为什么不统一?

Redis StreamRabbitMQ
项目里的角色秒杀订单削峰(轻量)跨服务业务消息(可靠)
可靠性依赖 RDB/AOF 持久化ACK + 磁盘持久化 + 死信队列
延迟消息不原生支持延迟交换机插件
额外组件零额外(已有 Redis)需要 RabbitMQ 服务
路由灵活性Topic 级别Exchange(Direct/Topic/Fanout)

秒杀走 Stream 的原因:秒杀链路已经重度依赖 Redis——库存、去重、候补全在 Redis 里完成。用 Stream 把消息投递也放在同一个 Lua 脚本里,"扣库存 + 发消息"原子完成,不引入额外中间件。

跨服务消息走 RabbitMQ 的原因:订单通知、积分发放这些消息每一条都很重要、不能丢。RabbitMQ 的 ACK + 死信队列 + 延迟交换机组合比 Redis Stream 更适合做"业务消息总线"。

如果只能选一个:选 RabbitMQ。它能同时覆盖秒杀订单消费和跨服务消息两个场景。Redis Stream 更多是为了展示"知道什么时候该轻量、什么时候该引入中间件"的判断力——这恰恰是面试官想听到的。


九、Feed 流推送——ZSet 收件箱 + 时间游标分页

面试官问:Feed 流怎么实现的?



推模式(我项目里用的)

用户发动态 → 把动态 ID 写入每个粉丝的 Redis ZSet 收件箱。粉丝打开页面 → 直接从自己的收件箱按时间倒序拉取。

// 发布动态(写扩散)longscore=System.currentTimeMillis();for(LongfollowerId:followerIds){redis.opsForZSet().add("feed:inbox:"+followerId,blogId.toString(),score);// 每个收件箱最多保留 1000 条redis.opsForZSet().removeRange("feed:inbox:"+followerId,0,-1001);}// 读取(时间游标分页,替代 offset)Set<ZSetOperations.TypedTuple<String>>result=redis.opsForZSet().reverseRangeByScoreWithScores("feed:inbox:"+userId,0,cursor-1,// cursor = 上一页最后一条的 score0,size);

游标分页 vs offset 分页

offset 在两个请求之间如果有新数据插入,翻页会出现重复。游标分页用 score 定位——“给我 score 小于上一页最后一条时间戳的前 10 条”,天然对增量免疫。

推模式 vs 拉模式

  • :发动态时扩散到粉丝收件箱,读的时候 O(1)。适合粉丝少的场景。
  • :发动态只写一条,读的时候聚合关注列表。适合大 V 场景。
  • 大 V 方案:推拉结合——普通用户推,大 V 拉(微博/推特的经典架构)。

十、GEO 附近商户——底层是个 ZSet

面试官问:Redis GEO 底层是什么?

就是 ZSet。Redis 帮你做了经纬度 → GeoHash → Score 的转换。



GeoHash 编码原理

把二维经纬度编码成一维字符串。对经度 [-180, 180] 和纬度 [-90, 90] 交替做二分逼近,每次二分确定一个 bit,然后按 Base32 编码成字符。

以南昌红谷滩(115.85°E, 28.70°N)为例:

  1. 经度二分:115.85 在 [0, 180] 右半 → bit=1 → 区间缩到 [0, 180] → 继续在左半 → bit=0 → 区间缩到 [0, 90] → 继续在右半 → bit=1 → …
  2. 纬度二分:28.70 在 [-90, 90] 右半 → bit=1 → 区间缩到 [0, 90] → 继续在左半 → bit=0 → …
  3. 交替合并 bit,每 5 位转一个 Base32 字符 → 最终得到类似wtc6v的字符串

关键性质:前缀越相同,位置越接近(但有边界效应——刚好在赤道两侧的两个点可能被漏掉,所以GEOSEARCH会同时搜周围 8 个格子)。

5km 毫秒级返回的原因

ZSet 的 score 范围查找是 O(log N + M),内存操作,没有磁盘 IO。

// GEOSEARCH:6.2+ 统一替代 GEORADIUSGeoResults<GeoLocation<String>>results=redis.opsForGeo().search("shop:geo",newGeoCoordinate(longitude,latitude),newDistance(5,DistanceUnit.KILOMETERS),GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().sortAscending().limit(20));

十一、集群——主从、哨兵、Cluster,别再搞混了

面试官问:Redis 有哪几种集群模式?什么时候用哪种?

你要答

Redis 的高可用演进路径是主从 → 哨兵 → Cluster,三个方案解决的是不同层次的问题。

模式核心思路解决的问题局限
主从复制一主多从,读写分离。从节点异步复制主节点数据。读压力分担 + 数据冗余备份主节点挂了要手动切换,无法自动故障转移
哨兵(Sentinel)在主从基础上加 Sentinel 进程集群(至少 3 个),监控主节点健康状态。主挂了自动选举新主。自动故障转移(高可用)本质还是单主架构,所有写操作仍走一个节点,无法水平扩展写能力
Cluster去中心化,数据按 slot 分片存储在多组主从节点上。每组负责一部分 slot。水平扩展(海量数据 + 高并发写)不支持多 Key 跨 slot 操作(除非用 hash tag),客户端要支持重定向

Cluster 分片机制

Cluster 把整个 key 空间分为16384 个 slot(哈希槽):

slot = CRC16(key) % 16384

每个 Master 节点负责一部分 slot(比如 3 个 Master 各负责约 5461 个 slot)。客户端可以请求任意节点——如果 key 不在当前节点的 slot 范围,节点返回MOVED重定向,客户端自动跳转到正确的节点。

为什么是 16384?这个数字是作者权衡的结果——足够大来均匀分布(每个节点能分到充足 slot 数),又足够小让节点间 gossip 协议的心跳包不携带过大的 slot 位图。16384 个 slot 的状态用 2KB 的位图就能表示。

CAP 取舍

Redis Cluster 在分布式场景下:

  • P(分区容错)必须保证——网络分区是不可避免的
  • 当半数以上 Master 挂了,整个集群拒绝服务——牺牲 A(可用性)保 C(一致性)
  • 但单个 slot 的主从切换是秒级的(Sentinel 类似机制)——尽可能减少不可用窗口

面试常见误区:很多人以为 Cluster = 多主多从 + 自动切换 + 横向扩展一条龙。实际上 Cluster 的核心价值是数据分片——解决的是单机存不下、写不下的问题。如果数据量不大、QPS 不高,主从 + 哨兵就够了,上 Cluster 反而增加了运维复杂度和客户端适配成本。


总结

Redis 面试的核心就五个方向:

方向必考点
数据结构ZSet 跳表、GeoHash 编码、Stream 消费模型
缓存三防穿透/击穿/雪崩各怎么解决,请求合并 vs SETNX
持久化RDB vs AOF vs 混合持久化,fork 阻塞原因
项目实战Lua 原子性、Redis 和 MySQL 一致性兜底、延迟队列、分布式锁
集群主从/哨兵/Cluster 区别,Cluster slot 分片

面试中回答 Redis 问题的核心技巧:不要只背八股。面试官问"你用过 Redis 的什么数据结构",你回答"ZSet",这叫 10 秒的回答。你回答"我用 ZSet 做了 Feed 流收件箱,时间游标分页替代 offset 避免翻页重复,每个收件箱上限 1000 条",这就是 2 分钟的回答——面试官会顺着 Feed 流继续问,你就把自己准备好的东西全聊出来了。

记住三个"不是"

  • Redis 不是只有缓存——它在项目里承担了 7 种不同角色
  • 面试不是在考试——不要只背概念,每个知识点绑一个项目场景
  • 深度不在第一问——面试官的套路是顺着一个点一路深挖,你要做的是在每一层都准备好"下一层的答案"

这篇文章整理了我在面试中被问到的 Redis 高频题。如果有帮助,欢迎评论区交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询