面试官接连追问 Redis,我用项目实战扛了 40 分钟

面试官接连追问 Redis,我用项目实战扛了 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 查 scoreskiplist 用来 O(log n) 做范围查询和排序。跳表原理面试高频跳表就是多层有序链表。最底层是一个完整的有序双向链表每往上一层节点数约减半——通过随机函数决定一个节点是否晋升到上一层。查找的时候从最高层开始类似二分查找下一个节点 score 大于目标就降一层直到找到或者确认不存在。为什么用跳表不用红黑树跳表实现比红黑树简单——旋转和变色真的容易写出 bug跳表天然支持范围查询底层是双向链表找到起点后顺着往下扫就行红黑树范围查询需要中序遍历麻烦一些其他数据结构的底层简表数据结构底层实现Stringint / embstr≤44字节/ rawListquicklistlinkedlist ziplist 混合Hashlistpack7.0/ ziplist → 数据多转 hashtableSetintset全整数少元素→ 转 hashtableZSetlistpack7.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(datanull){redis.set(key,,2,TimeUnit.MINUTES);// 2分钟后过期returnnull;}防击穿最爱追问请求合并 vs 互斥锁// 请求合并只放一个请求去查 DB其余的等结果共享privateConcurrentHashMapString,CompletableFutureStringflyMapnewConcurrentHashMap();publicStringget(Stringkey){Stringcachedredis.get(key);if(cached!null)returncached;// 同一个 key 只有一个 飞行中 的请求CompletableFutureStringfutureflyMap.computeIfAbsent(key,k-CompletableFuture.supplyAsync(()-{StringdbDatadb.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 加随机抖动避免集体过期intttl30*60ThreadLocalRandom.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到期了怎么删惰性删除访问 Key 的时候顺便检查有没有过期 → 过期了删掉。优点是对 CPU 友好缺点是有过期 Key 一直没人访问就占着内存。定期删除每 100ms 随机抽一批 Key 检查过期就删每次执行不超过 25ms。两者结合定期清理大部分惰性兜底。内存淘汰策略内存满了新写入怎么办共 8 种策略行为noeviction不淘汰写入直接报错默认策略allkeys-lru所有 Key 中淘汰最久未使用的allkeys-lfu4.0所有 Key 中淘汰最少使用频率的allkeys-random所有 Key 中随机淘汰volatile-lru只在设了过期时间的 Key 中淘汰最久未使用的volatile-lfu4.0只在设了过期时间的 Key 中淘汰最少使用的volatile-random只在设了过期时间的 Key 中随机淘汰volatile-ttl淘汰 TTL 最短最早过期的LRU vs LFULRU 看最近一次什么时候用的——适合热点有明显时间特征的场景。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. 校验库存localstocktonumber(redis.call(GET,KEYS[1]))ifstocknilorstock0thenredis.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 写失败了怎么办四层兜底Lua 脚本先把关前置拦截校验不通过根本不扣Redisson 分布式锁做 DB 层二次去重同一用户同一券只放一个进 DBMySQL 条件更新兜底UPDATE ... SET stock stock - 1 WHERE stock 0affected_rows0 就不扣定时对账异步比对 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 级别ExchangeDirect/Topic/Fanout秒杀走 Stream 的原因秒杀链路已经重度依赖 Redis——库存、去重、候补全在 Redis 里完成。用 Stream 把消息投递也放在同一个 Lua 脚本里扣库存 发消息原子完成不引入额外中间件。跨服务消息走 RabbitMQ 的原因订单通知、积分发放这些消息每一条都很重要、不能丢。RabbitMQ 的 ACK 死信队列 延迟交换机组合比 Redis Stream 更适合做业务消息总线。如果只能选一个选 RabbitMQ。它能同时覆盖秒杀订单消费和跨服务消息两个场景。Redis Stream 更多是为了展示知道什么时候该轻量、什么时候该引入中间件的判断力——这恰恰是面试官想听到的。九、Feed 流推送——ZSet 收件箱 时间游标分页面试官问Feed 流怎么实现的推模式我项目里用的用户发动态 → 把动态 ID 写入每个粉丝的 Redis ZSet 收件箱。粉丝打开页面 → 直接从自己的收件箱按时间倒序拉取。// 发布动态写扩散longscoreSystem.currentTimeMillis();for(LongfollowerId:followerIds){redis.opsForZSet().add(feed:inbox:followerId,blogId.toString(),score);// 每个收件箱最多保留 1000 条redis.opsForZSet().removeRange(feed:inbox:followerId,0,-1001);}// 读取时间游标分页替代 offsetSetZSetOperations.TypedTupleStringresultredis.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为例经度二分115.85 在 [0, 180] 右半 → bit1 → 区间缩到 [0, 180] → 继续在左半 → bit0 → 区间缩到 [0, 90] → 继续在右半 → bit1 → …纬度二分28.70 在 [-90, 90] 右半 → bit1 → 区间缩到 [0, 90] → 继续在左半 → bit0 → …交替合并 bit每 5 位转一个 Base32 字符 → 最终得到类似wtc6v的字符串关键性质前缀越相同位置越接近但有边界效应——刚好在赤道两侧的两个点可能被漏掉所以GEOSEARCH会同时搜周围 8 个格子。5km 毫秒级返回的原因ZSet 的 score 范围查找是 O(log N M)内存操作没有磁盘 IO。// GEOSEARCH6.2 统一替代 GEORADIUSGeoResultsGeoLocationStringresultsredis.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 高频题。如果有帮助欢迎评论区交流。