Skip to content

Spring集成Redis面试题库 ​

生成时间:2026-05-23 16:27


【面试题】1. Redis的ziplist和listpack有什么区别?为什么Redis 7.0要把Hash/List的底层编码从ziplist替换为listpack? ​

【标准答案】ziplist是Redis早期设计的紧凑连续内存结构,每个entry保存前一个entry的长度(prevlen字段),用于反向遍历。问题在于:当中间某个entry修改导致长度变化时,后续所有entry的prevlen都可能需要级联更新,最坏情况O(n²)复杂度,这就是经典的"连锁更新"问题。

listpack是ziplist的替代方案,核心改动是:每个entry不再保存前一个entry的长度,而是保存自身长度。反向遍历改为从当前entry的自身长度回退定位。这样修改任何一个entry都不会影响其他entry,彻底消除了连锁更新问题。

Redis 7.0将Hash、ZSet的底层编码从ziplist迁移到listpack,就是解决这个性能隐患。面试时要点明:listpack是ziplist的演进版本,保留了内存紧凑的优势,同时消除了连锁更新的致命缺陷。


【面试题】2. Spring Data Redis中Lettuce和Jedis有什么区别?生产环境怎么选? ​

【标准答案】核心区别三点:

第一,线程模型不同。Jedis是同步阻塞的,每个操作阻塞当前线程等待响应,多线程下需要连接池,每个连接同一时刻只能处理一个请求。Lettuce基于Netty,是异步非阻塞的,单个连接可以复用,支持管道和响应式编程。

第二,连接管理不同。Jedis实例不是线程安全的,多线程必须用连接池(JedisPool)。Lettuce的连接是线程安全的,一个连接可以被多线程共享,省去了连接池的开销,当然也可以配合连接池做更细粒度的控制。

第三,功能支持不同。Lettuce原生支持集群、哨兵、集群拓扑刷新、异步/响应式API。Jedis在集群和异步方面支持较晚较弱。

生产环境推荐Lettuce(Spring Boot 2.x起默认就是Lettuce)。例外场景:如果你的业务大量使用阻塞命令(BLPOP、BRPOP),Lettuce的单连接复用可能导致阻塞蔓延,这时候可以考虑Jedis配合连接池,或者用Lettuce的连接池模式隔离阻塞操作。

配置示例:Spring Boot 2.x默认引入lettuce-core,如果要换Jedis,排除lettuce依赖、引入jedis依赖,然后spring.redis.client-type=jedis。


【面试题】3. Redis AOF重写是什么原理?重写期间如果有新写入怎么办? ​

【标准答案】AOF重写的本质是:根据Redis当前内存中的数据状态,生成一份最精简的AOF文件,而不是逐条分析旧AOF。比如对一个key执行了100次SET,重写后只保留最后一次SET,文件体积大幅缩小。

重写流程:

  1. 主进程fork出子进程执行重写(类似bgsave)
  2. 子进程遍历内存数据,生成新的AOF文件
  3. 重写期间的新写入操作:主进程除了写原有AOF缓冲区,还写入一块"AOF重写缓冲区"
  4. 子进程完成重写后,主进程将重写缓冲区的增量数据追加到新AOF文件末尾
  5. 用新AOF文件原子替换旧AOF文件

关键设计:重写缓冲区保证重写期间的数据不丢失。fork子进程利用了操作系统的写时复制(COW),子进程读取fork时的内存快照,主进程的修改不会影响子进程。

面试加分点:AOF重写触发条件可以通过auto-aof-rewrite-percentage和auto-aof-rewrite-min-size配置。混合持久化下,重写后的AOF文件前半段是RDB格式的二进制数据,后半段是AOF格式的增量命令,兼顾恢复速度和数据安全。


【面试题】4. Spring Cache的@Cacheable注解中condition和unless有什么区别?实际怎么用? ​

【标准答案】两者的执行时机和作用完全不同:

condition在方法执行前判断,决定是否要走缓存逻辑。如果condition=false,整个缓存逻辑被跳过,方法直接执行,结果也不缓存。语法用SpEL表达式,能访问参数但访问不了返回值(因为还没执行)。

unless在方法执行后判断,决定是否要把结果存入缓存。如果unless=true,方法正常执行并返回结果,但不缓存。unless可以访问返回值(用#result引用),适合根据返回值决定是否缓存。

典型用法:

  • condition:只对特定参数值走缓存,如condition="#type == 'hot'"——只有热点类型的数据才缓存
  • unless:排除空值或不该缓存的结果,如unless="#result == null"——空结果不缓存,防止缓存穿透时存大量空值可以设TTL;unless="#result.size() > 10000"——大结果集不缓存

组合使用:@Cacheable(value="user", condition="#id != null", unless="#result == null")——参数为null不走缓存,结果为null不缓存。

注意坑点:condition=false时方法每次都执行,不会走缓存也不会存缓存;unless=true时方法正常执行返回,只是不存缓存。如果只想跳过缓存读取但仍想写入,应该用@CachePut配合condition。


【面试题】5. Redis Cluster的16384个槽位是怎么分配的?为什么不是65536? ​

【标准答案】Redis Cluster将整个key空间划分为16384个槽(slot),每个key通过CRC16(key) % 16384计算所属槽位,每个节点负责一部分槽位。

为什么是16384而不是65536?这是面试常问的点:

  1. 心跳包大小:节点之间通过Gossip协议通信,心跳包中要携带节点负责的槽位信息。65536个槽位需要8KB的bitmap,16384只需要2KB。集群节点频繁通信,2KB的心跳包在集群规模较大时对带宽更友好。

  2. 集群规模上限:Redis Cluster官方建议最大1000个节点。16384个槽位分配给1000个节点,每个节点约16个槽,够用了。65536对当前集群规模没有实际意义。

  3. 槽位迁移粒度:16384的粒度足够细,迁移时可以按槽为单位平滑迁移,不影响服务。

槽位分配方式:可以通过CLUSTER ADDSLOTS手动分配,也可以用redis-cli --cluster create自动均匀分配。扩容时用CLUSTER SETSLOT MIGRATING/IMPORTING做槽位迁移,迁移过程中请求会通过ASK/MOVED重定向。

面试加分:客户端应该实现智能路由(smart client),缓存槽位与节点的映射关系,直接请求目标节点,避免重定向开销。Spring Data Redis的Lettuce客户端默认支持智能路由。


【面试题】6. Redis分布式锁用SET NX EX实现有什么问题?Redisson是怎么解决的? ​

【标准答案】SET NX EX是最简单的分布式锁实现:SET lock_key unique_value NX EX 30。存在几个典型问题:

问题1:锁超时误释放。业务没执行完锁就过期了,其他线程拿到锁,导致并发问题。解决:Redisson用看门狗(Watchdog)机制,默认锁30秒,每10秒(1/3超时时间)自动续期,业务没完成就持续续期。

问题2:误删别人的锁。线程A的锁超时后,线程B拿到锁,线程A执行完执行DEL把B的锁删了。解决:value设为唯一标识(UUID+线程ID),释放时先判断value是否一致再删除。Redisson的锁value是"UUID:线程ID",释放时用Lua脚本原子判断+删除。

问题3:不可重入。同一线程拿不到自己持有的锁。解决:Redisson用Hash结构实现可重入,field是线程标识,value是重入次数,每次加锁value+1,解锁value-1,到0才真正释放。整个逻辑用Lua脚本保证原子性。

问题4:集群下的锁安全性。主节点加锁成功但未同步到从节点就宕机,从节点晋升后锁丢失。Redisson提供RedLock算法,向N个独立节点加锁,超过半数成功才算获取锁。但RedLock有争议(Martin Kleppmann质疑),生产中更推荐单节点+Redlock降级或用ZooKeeper做强一致锁。

Spring Boot集成:引入redisson-spring-boot-starter,直接用RLock lock = redissonClient.getLock("key"); lock.lock()即可,自动续期、可重入、原子释放全部内置。


【面试题】7. 缓存和数据库双写一致性有哪些方案?你们生产用的哪种? ​

【标准答案】常见方案四种:

1. Cache Aside(旁路缓存):读操作先查缓存,miss则查DB再写缓存;写操作先更新DB再删缓存。这是最主流的方案。问题在于极端并发下,删缓存失败或时序问题会导致不一致。解决:延迟双删——更新DB后删缓存,延迟一段时间再删一次,覆盖掉并发写带来的脏数据。

2. Read Through:读操作只查缓存,miss时由缓存层自动从DB加载。业务代码不直接操作DB读。Spring Cache的@Cacheable本质就是Read Through模式。

3. Write Through:写操作先写缓存,由缓存层同步写DB。缓存和DB始终保持一致,但写入延迟增加。

4. Write Behind(异步回写):写操作只写缓存,由后台异步批量写DB。吞吐量高但有一致性窗口,宕机可能丢数据。

生产实践推荐:Cache Aside + 延迟双删 + 消息队列兜底。更新DB后删缓存,发一条MQ消息,消费者再删一次缓存。如果要求更强一致性,用Canal监听MySQL binlog,binlog变更时自动更新/删除缓存,这样应用层不需要关心缓存更新,完全解耦。

关键注意点:

  • 为什么是删缓存而不是更新缓存?因为并发写时更新缓存可能后写覆盖先写(写写冲突),删缓存下次读自然加载最新值。
  • 缓存加合理TTL兜底,即使删缓存失败,过期后也能最终一致。
  • Spring Cache的@CacheEvict就是删缓存,配合@Cacheable形成Cache Aside模式。

【面试题】8. Redis的Stream和List做消息队列各有什么优缺点?Stream的消费者组是怎么工作的? ​

【标准答案】List做消息队列:

  • 优点:简单,LPUSH + BRPOP就能实现,支持阻塞读取
  • 缺点:没有消费确认机制,消息消费后即删除,无法回溯;不支持消费者组;不支持消息持久化回溯;一条消息只能被一个消费者消费(点对点模式)
  • 适用:简单任务队列,一对多场景用Pub/Sub补充

Stream做消息队列:

  • 优点:支持消费者组(Consumer Group),消息可被多个组独立消费;支持ACK确认(XACK),未确认的消息可重新分配;支持消息回溯(XPENDING/XCLAIM);消息ID自带时间戳且严格递增;类似Kafka的消费者组模型
  • 缺点:比List复杂;没有Kafka那样成熟的分区机制(单Stream单分片);大消息量下单Stream有瓶颈

Stream消费者组工作流程:

  1. XGROUP CREATE创建消费者组,指定从哪个ID开始消费(0-0从头,$从最新)
  2. XREADGROUP GROUP组名消费者名 COUNT数量 BLOCK超时 STREAMS streamkey >——消费者从组中读取消息,>表示未消费过的消息
  3. 消费者处理完消息后XACK确认,消息从Pending列表移除
  4. XACK未被确认的消息留在Pending列表,其他消费者可以用XPENDING查看、XCLAIM转移处理
  5. XAUTOCLAIM自动转移超时未确认的消息,实现消息重试

Spring Data Redis集成:用RedisTemplate直接操作Stream命令,或用StreamMessageListenerContainer做消息监听。配合@StreamListener注解(Spring Cloud Stream)可以对接更高级的消息模型。


【面试题】9. Redis的BigKey怎么发现?怎么拆分?有什么实际危害? ​

【标准答案】发现方法:

  1. redis-cli --bigkeys:扫描各数据类型的最大key
  2. redis-cli --memusage:查看指定key的内存占用(不推荐在线上频繁执行,计算成本高)
  3. SCAN + DEBUG OBJECT:逐个扫描分析,适合定制化检测
  4. INFO memory中的mem_fragmentation_ratio观察内存碎片率异常
  5. RDB分析工具:redis-rdb-tools离线分析RDB文件,生成内存报告
  6. SLOWLOG:慢查询中频繁出现的key可能是BigKey

实际危害:

  • 内存不均:集群模式下某节点内存远高于其他节点
  • 阻塞:DEL大key阻塞主线程(Hash/ZSet删除O(N)),导致服务假死
  • 网络阻塞:大key传输占满带宽,影响其他请求
  • 持久化阻塞:fork时COW拷贝大key增加内存压力

拆分方案:

  • 大Hash:按field分段,如user:1000分成user:1000:basic、user:1000:profile等,每个子Hash控制在500-1000个field
  • 大List:按范围分段,如queue:0、queue:1,消费者轮询读取
  • 大Set:按元素hash取模分段,如tags:shard0、tags:shard1
  • 大ZSet:按score范围分段

删除BigKey:不要直接DEL,用UNLINK(Redis 4.0+)异步删除,主线程只标记删除,后台线程回收内存。或者分批删除:HSCAN + HDEL、SSCAN + SREM、ZSCAN + ZREM。

Spring Data Redis注意:RedisTemplate的delete方法会同步DEL,BigKey建议用execute执行UNLINK命令。


【面试题】10. Redis 7.x的Function特性是什么?和传统Lua脚本有什么区别? ​

【标准答案】Redis 7.0引入了Function,是Lua脚本的升级替代方案:

传统Lua脚本的问题:

  1. 脚本散落各处,没有统一管理,不同客户端各自维护脚本
  2. EVAL/EVALSHA每次都要传脚本或SHA1,管理不便
  3. 脚本缓存依赖Redis内存,重启后丢失
  4. 没有版本管理,更新脚本只能替换

Function的改进:

  1. 函数库(Library)概念:将相关函数组织成库,注册到Redis中持久化存储(RDB/AOF都会保存),重启不丢失
  2. FUNCTION LOAD注册:用FUNCTION LOAD命令注册函数库,后续直接用FCALL调用,不用每次传脚本
  3. FCALL调用:替代EVAL,语法更清晰——FCALL 函数名 numkeys key [key...] arg [arg...]
  4. 函数库管理:FUNCTION LIST查看已注册库,FUNCTION DELETE删除库,FUNCTION FLUSH清空所有库
  5. 代码组织:一个库可以包含多个函数,函数之间可以共享辅助逻辑,模块化程度更高

Function和Lua脚本的核心区别:

  • 持久化:Function默认持久化到RDB/AOF,Lua脚本缓存重启丢失
  • 管理:Function有库的概念,Lua脚本是散装的
  • 调用:FCALL比EVAL更直观,不需要传完整脚本或SHA1
  • 兼容:Function底层还是Lua引擎,现有Lua脚本逻辑可以平滑迁移

迁移方式:把EVAL中的Lua逻辑封装成Function注册即可。Spring Data Redis 3.x开始支持Function API,可以用RedisTemplate.execute(RedisScript)兼容旧方式,新项目建议用Function。

面试加分:提一下Redis 7.0还引入了Function的FLUSH SYNC/ASYNC模式,以及ACL对Function的权限控制(-@function限制),体现对新特性的深度了解。