Skip to content

本轮 Spring集成Redis面试题库(带答案) ​

生成时间:2026-05-22 00:05

【面试题】1. Redis的ZSet底层编码有哪几种?什么条件下会从ziplist/listpack跳转到skiplist?在排行榜场景中编码转换会带来什么影响? ​

【标准答案】Redis 7.0之前ZSet底层有两种编码:ziplist和skiplist+hashtable。7.0之后ziplist被替换为listpack。转换条件:当元素数量超过zset-max-listpack-entries(默认128)或任意单个元素长度超过zset-max-listpack-value(默认64字节),就会从listpack跳转为skiplist+hashtable双结构。skiplist负责范围查询,hashtable负责O(1)按成员查分数。排行榜场景中,如果榜单规模小(比如Top100),数据大概率还停留在listpack,内存紧凑、CPU缓存友好;一旦数据膨胀超过阈值触发编码转换,内存占用会突增(skiplist每个节点约32字节额外开销),但范围查询性能不会退反而更优,因为skiplist本身就是为范围查询设计的。面试关键点:说清楚"小数据用listpack省内存,大数据用skiplist保性能"的设计权衡,以及7.0的listpack替代ziplist是为了解决ziplist级联更新问题。

【面试题】2. AOF重写的触发机制是什么?重写期间如果有新写入命令怎么办?AOF重写和RDB bgsave能同时进行吗? ​

【标准答案】AOF重写触发有两种方式:手动触发(BGREWRITEAOF命令)和自动触发(同时满足auto-aof-rewrite-min-size和auto-aof-rewrite-percentage两个条件)。重写期间新写入的命令会同时追加到旧AOF缓冲区和重写缓冲区(aof_rewrite_buf),重写完成后将重写缓冲区的命令追加到新AOF文件末尾,再原子替换旧文件,保证数据零丢失。AOF重写和RDB bgsave不能同时进行——Redis源码中会检测是否有bgsave或bgrewriteaof在执行,如果有就拒绝另一个操作,避免两个子进程同时大量写磁盘造成IO竞争和内存翻倍压力。面试关键点:重写缓冲区机制是保证重写期间数据不丢失的核心;不能同时执行是性能保护策略。

【面试题】3. Spring Data Redis中Lettuce和Jedis的核心区别是什么?为什么Spring Boot 2.x之后默认选择Lettuce?生产环境怎么选? ​

【标准答案】核心区别三点:(1)线程模型:Jedis是同步阻塞式,每个操作独占连接,多线程需连接池;Lettuce基于Netty实现异步非阻塞,单个连接可被多线程共享,状态线程安全。(2)连接方式:Jedis是直连模式;Lettuce默认单连接复用+按需扩连接池。(3)功能:Lettuce原生支持集群拓扑刷新、管道、发布订阅、响应式编程(Reactive)。Spring Boot 2.x默认选Lettuce因为单连接复用省资源、异步性能更好、响应式生态更友好。生产选型建议:如果项目是传统同步架构且历史代码大量用Jedis API,继续Jedis没毛病;新项目、高并发、响应式场景果断Lettuce。注意Lettuce集群模式下要配置adaptiveTopologyRefresh开启拓扑自动刷新,否则集群节点变更会连接失败。

【面试题】4. @Cacheable的condition和unless有什么区别?如果我只想缓存查询结果不为空的数据,怎么配?如果key冲突了会怎样? ​

【标准答案】condition是在方法执行前判断,决定是否走缓存逻辑(为false则直接执行方法,不查缓存也不写缓存);unless是在方法执行后判断,决定是否将结果写入缓存(为true则不写入)。只想缓存非空结果:@Cacheable(value="user", key="#id", unless="#result == null")。这里用unless而不是condition,因为condition执行时还没有结果,判断不了。key冲突问题:如果不同方法用同一个cacheName和相同的key值,缓存会互相覆盖——比如findById(1)和findByName(1)如果key都解析成"1",后写入的会覆盖前一个。解决方案:key生成策略要包含业务前缀,如key="'user:id:' + #id"和key="'user:name:' + #name",或者自定义KeyGenerator。面试关键点:condition=前置门卫,unless=后置过滤器;key隔离是生产必做的。

【面试题】5. 缓存穿透、缓存击穿、缓存雪崩分别是什么?各自的经典解决方案是什么?项目中你们怎么做的? ​

【标准答案】缓存穿透:查询的数据在缓存和DB中都不存在,请求直接打到DB。解决方案:布隆过滤器前置拦截(适合数据相对固定的场景)、空值缓存设短TTL(适合数据动态变化的场景)、接口参数校验。缓存击穿:某个热点key过期瞬间,大量并发请求同时穿透到DB。解决方案:互斥锁(setnx抢锁,抢到查DB写缓存,没抢到sleep重试)、逻辑过期(缓存永不过期但存逻辑过期时间,过期后异步更新,期间返回旧数据)。缓存雪崩:大量key同时过期或Redis宕机,请求全部压到DB。解决方案:TTL加随机偏移量打散过期时间、多级缓存(本地Caffeine+Redis)、熔断降级(Hystrix/Sentinel)、Redis高可用(哨兵/集群)。项目实战:穿透用布隆过滤器+空值缓存双保险;击穿用Redisson分布式锁实现互斥更新;雪崩TTL随机散列+本地缓存兜底+Sentinel熔断。

【面试题】6. Redisson分布式锁的看门狗机制原理是什么?默认租约时间是多少?什么情况下看门狗会失效?如何避免? ​

【标准答案】看门狗(Watchdog)机制:Redisson加锁时如果不显式指定leaseTime,默认租约30秒(lockWatchdogTimeout),同时会启动一个定时任务,每10秒(30/3)检查锁还被持有就自动续期到30秒,直到显式解锁或持有锁的客户端宕机才停止续期。看门狗失效的场景:(1)加锁时指定了leaseTime,看门狗不启动,锁到时间自动释放;(2)业务线程被kill或OOM导致Redisson进程异常退出,看门狗线程也挂了,锁到期自动释放——这是预期行为,防止死锁;(3)Redisson客户端与Redis连接断开且无法重连,续期命令发不出去。避免失效:(1)不要随意指定leaseTime,让看门狗自动续期;(2)确保业务代码在finally中unlock;(3)高可用环境下用RedLock或Redisson的联锁(MultiLock),但要注意RedLock的争议(Martin Kleppmann的批评);(4)极端情况加锁续期失败要有降级预案。

【面试题】7. Redis Cluster为什么选择16384个槽位而不是65536?节点间通信用的什么协议?客户端怎么知道key该路由到哪个节点? ​

【标准答案】16384槽位的原因:(1)集群节点间通过Gossip协议交换节点信息和槽位映射,ping/pong消息中携带的槽位信息用bitmap表示,16384个槽只需2KB bitmap,65536需要8KB,集群规模大时带宽开销显著增加;(2)Redis Cluster设计时认为集群节点规模不会超过1000个,16384个槽每个节点平均分到16个槽已经足够,过多槽位增加管理复杂度没有收益;(3)槽位信息在客户端也需要缓存,更小的bitmap节省客户端内存。节点间通信用Gossip协议:每个节点定期随机选几个节点发送ping,收到后回复pong,包含自身状态和已知的其他节点信息,实现去中心化的状态传播。客户端路由:客户端缓存槽位映射表,对key做CRC16校验后对16384取模得到槽号,查映射表找到目标节点直接发送命令;如果目标节点不在(MOVED重定向),客户端更新映射表重试;如果是ASK重定向(槽位迁移中),临时重定向不更新映射表。

【面试题】8. 用Redis Stream实现消息队列时,消费者组(Consumer Group)怎么保证消息至少被消费一次?XACK的作用是什么?如果消费者宕机了,未确认的消息怎么处理? ​

【标准答案】消费者组保证at-least-once的核心机制:消费者通过XREADGROUP从Stream读取消息,消息进入PEL(Pending Entries List),只有消费者显式执行XACK后消息才从PEL中移除。如果消费者宕机,消息留在PEL中不会被丢失。处理未确认消息的方式:XPENDING命令查看PEL中长时间未确认的消息,然后用XCLAIM命令将消息转移给其他活跃消费者重新消费。生产实践中通常做法:启动一个定时任务,周期性调用XPENDING检查每个消费者IDLE时间过长的消息,超时后XCLAIM转移并重新投递。和Kafka的对比:Stream没有像Kafka那样基于Partition的多消费者并行度保证,单个Stream的消费者组内并行度取决于消费者数量。Spring Data Redis中可用StreamMessageListenerContainer实现消费监听,配置Consumer和StreamOffset即可。面试关键点:PEL机制+XACK确认+XCLAIM转移=消息可靠性三件套。

【面试题】9. 用Redis实现滑动窗口限流的Lua脚本怎么写?和固定窗口限流比有什么优势?Spring Boot项目中怎么集成? ​

【标准答案】滑动窗口限流Lua脚本核心逻辑:用ZSet存储请求时间戳,key为限流标识,score为请求时间戳,value也为时间戳(或唯一ID防重复)。每次请求时:(1)ZREMRANGEBYSCORE移除窗口外的旧记录;(2)ZCARD统计窗口内请求数;(3)如果未超限,ZADD加入当前请求;(4)EXPIRE设置key过期时间。Lua脚本保证原子性。和固定窗口比的优势:固定窗口在窗口边界会出现2倍流量突刺(比如限流100/分钟,0:59秒涌入100个,1:01秒又涌入100个,2秒内实际200个请求);滑动窗口以当前时刻为基准往前推一个窗口,不存在边界突刺问题。Spring Boot集成:自定义注解@RateLimit+AOP切面,切面中调用Lua脚本,用RedisTemplate.execute(RedisScript, keys, args)执行。也可以用Redisson的RRateLimiter,它内部就是基于滑动窗口+Lua实现的。面试关键点:Lua保证原子性、ZSet存储实现滑动、和固定窗口的边界突刺对比。

【面试题】10. Redis 7.x的Function和传统的Lua脚本eval相比有什么优势?生产环境迁移需要注意什么?Client-Side Cache解决了什么问题? ​

【标准答案】Function替代Lua脚本的优势:(1)持久化管理:Lua脚本通过evalsha执行,脚本不在Redis持久化中,重启后需要重新加载;Function用FUNCTION LOAD注册后会持久化到AOF/RDB,重启自动恢复;(2)库(Library)概念:Function支持将多个函数组织在一个库中,按库管理(FUNCTION LIST/DELETE/FLUSH),比evalsha散乱管理更清晰;(3)更丰富的API:Function可以使用Redis 7提供的新回调API,支持更复杂的逻辑;(4)更安全:Function有FLAG标记(可声明是否写操作、是否允许随机等),比Lua脚本更可控。迁移注意:Function语法从Lua变成了Redis Function格式(虽然底层还是Lua),需要改写脚本;旧项目大量eval/evalsha调用需要逐步替换;如果用了Redis Cluster,Function需要每个主节点都加载。Client-Side Cache:让客户端本地缓存热点数据,当Redis中数据变更时通过invalidation通知客户端失效,减少网络往返。Redis 6+通过CLIENT TRACKING命令开启,支持广播模式和转发模式。Spring Data Redis 3.x已支持,适合读多写少的场景,可以显著降低Redis压力和延迟。