Skip to content

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

生成时间:2026-05-24 21:10


【面试题】1. Redis的String类型底层编码int、embstr、raw分别在什么条件下使用?为什么embstr只适用于短字符串? ​

【标准答案】 Redis String底层有三种编码:

  • int:当值可以用long类型整数表示时(如SET key 123),直接用int编码,不需要额外分配SDS结构,零开销。
  • embstr:当字符串长度 ≤ 44字节(Redis 5.0+,早期版本是39字节)时使用。embstr是一次malloc分配,把redisObject和SDS连续存放在一起,内存紧凑、缓存友好。
  • raw:字符串长度 > 44字节时使用。raw是两次malloc,redisObject和SDS分开分配,可以通过sdscat等函数追加修改。

embstr为什么只适合短字符串?因为embstr是只读的——redisObject和SDS连续存放,没有独立SDS头部,无法直接扩容。任何修改操作(如APPEND)都会把embstr升级为raw。所以embstr本质上是一个"创建后不可变"的优化,只对短字符串有效。

面试加分点:可以提一下Redis 7.0用listpack替代ziplist后,embstr的阈值有所调整。


【面试题】2. Redis RDB持久化的bgsave完整流程是怎样的?fork子进程期间主线程会阻塞吗?COW机制如何保证数据一致性? ​

【标准答案】 bgsave流程:

  1. 主进程调用bgsave命令,Redisfork出子进程。fork瞬间主线程会短暂阻塞(复制页表,通常几毫秒到几十毫秒,取决于内存大小)。
  2. fork完成后,子进程开始遍历内存生成RDB文件,主进程继续正常处理请求。
  3. 子进程写RDB时,利用操作系统的COW(Copy-On-Write)机制:主进程若修改某页数据,OS会先复制该页给主进程,子进程仍然读旧页。所以子进程看到的是fork时刻的一致性快照。
  4. RDB写完后,子进程退出,主进程通过信号得知完成,用临时RDB替换旧RDB。

关键点:

  • fork的阻塞时间与Redis内存大小成正比(页表越大,复制越慢),内存几十GB时可能阻塞上百毫秒,这是RDB的主要痛点。
  • COW的代价是额外内存开销——修改的页会复制一份,如果写操作密集,内存可能翻倍。
  • 生产环境建议用bgsave而非save,save是主线程同步生成RDB,会完全阻塞。

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

【标准答案】 核心区别:

维度JedisLettuce
连接方式直连,每个Jedis实例对应一个TCP连接基于Netty的连接池,单个连接可复用
线程安全不安全,多线程需用连接池(JedisPool)安全,单连接支持并发操作
异步不支持支持异步/响应式(Reactive)
集群手动管理分片连接自动拓扑刷新和分片
性能低并发下简单直接高并发下性能更优(连接复用+管道)

Spring Boot 2.x默认选Lettuce的原因:

  1. 连接复用:Lettuce基于Netty,一个TCP连接通过队列化命令支持多线程并发,资源利用率高。
  2. 天然支持响应式:配合WebFlux做响应式编程,Jedis做不到。
  3. 集群自适应:Redis Cluster节点变化时Lettuce能自动刷新拓扑,Jedis需要手动处理。
  4. Spring生态对齐:Lettuce的异步模型更契合Spring的响应式体系。

注意:如果项目是传统的同步阻塞架构且不需要异步,Jedis也没问题,不要盲目追新。但新项目建议Lettuce。


【面试题】4. 缓存穿透、缓存击穿、缓存雪崩分别是什么?各自的经典解决方案是什么? ​

【标准答案】 缓存穿透:查询的数据在数据库中根本不存在,缓存也无法命中,每次请求都打到数据库。

  • 解决方案:①布隆过滤器前置拦截——把所有可能存在的key放入布隆过滤器,不存在的直接返回;②空值缓存——查不到的key缓存null值,设短TTL(如2分钟)。
  • 面试建议说布隆过滤器为主、空值缓存兜底的双保险方案。

缓存击穿:某个热点key过期的瞬间,大量并发请求同时打到数据库。

  • 解决方案:①互斥锁——用SET NX抢锁,抢到的查库回填缓存,抢不到的等一下再读缓存;②逻辑过期——缓存不设TTL,数据里存逻辑过期时间,过期后异步更新,期间返回旧数据。
  • 互斥锁保证一致性但吞吐低,逻辑过期保证可用性但可能返回旧数据,根据业务选。

缓存雪崩:大量key同时过期或Redis宕机,请求全部涌向数据库。

  • 解决方案:①随机TTL——给过期时间加随机偏移,避免同时失效;②多级缓存——本地缓存(Caffeine)+ Redis,Redis挂了本地兜底;③熔断降级——数据库扛不住时直接降级返回默认值;④Redis高可用——哨兵/集群防宕机。

【面试题】5. Redisson分布式锁的看门狗(Watchdog)续期机制原理是什么?如果业务执行时间超过锁的默认30秒会怎样? ​

【标准答案】 Redisson分布式锁使用SET key uuid NX EX 30加锁,默认锁超时30秒。

看门狗机制:

  1. 加锁成功后,Redisson会启动一个定时任务(Watchdog),每隔锁超时时间/3(即10秒)检查一次。
  2. 如果当前线程仍持有锁(通过Lua脚本判断value是否匹配),就自动续期到30秒。
  3. 如果持有锁的客户端宕机,看门狗自然停止,锁30秒后自动过期释放。

业务超30秒:只要客户端还活着且没有主动释放锁,看门狗会持续续期,锁不会过期。只有以下情况锁才会丢失:

  • 客户端进程崩溃或重启 → 看门狗停止 → 30秒后锁过期
  • 显式调用lock.unlock() → 立即释放
  • 调用lock.lock(timeout, TimeUnit)指定了超时时间 → 不会启动看门狗,到期强制释放

面试关键:指定了leaseTime就不会启动看门狗,这是最常见的踩坑点。如果业务时间不确定,不要传leaseTime参数。


【面试题】6. Redis Cluster为什么使用16384个槽位而不是65536?节点间通信用的Gossip协议有什么优缺点? ​

【标准答案】 16384槽位的原因:

  1. 心跳包大小:集群节点定期交换心跳包,携带自身负责的槽位信息。16384个槽用2KB的bitmap就能表示(16384/8=2048字节),而65536需要8KB。节点越多、心跳越频繁,差距越明显。
  2. 集群规模合理上限:Redis Cluster建议不超过1000个节点,16384个槽完全够用。每个节点至少负责一个槽,1000个节点平均16个槽/节点,足够均匀。
  3. 槽位迁移开销:槽迁移时需要逐个slot处理,65536个槽迁移成本高很多。

Gossip协议:

  • 优点:去中心化,不需要一个中心节点做协调;节点增减时信息自然扩散;容错性好,单节点故障不影响信息传播。
  • 缺点:信息传播有延迟,可能存在短暂的节点状态不一致(如新加入的节点需要几轮才能被所有节点感知);集群大时Gossip消息量增大。

面试加分:可以提Redis Cluster的cluster-node-timeout配置影响故障检测速度,太大则故障转移慢,太小则网络抖动容易误判。


【面试题】7. Spring Cache的@Cacheable注解中condition和unless有什么区别?分别在什么场景下使用? ​

【标准答案】 condition:在方法执行之前判断,决定是否走缓存逻辑(包括查缓存和存缓存)。

  • condition = "#id > 100" → 只有id>100的请求才会查/写缓存
  • 不满足条件 → 直接执行方法,不查缓存也不存缓存

unless:在方法执行之后判断,决定是否将结果存入缓存。

  • unless = "#result == null" → 结果为null时不缓存
  • unless = "#result.size() > 1000" → 结果集太大时不缓存

典型使用场景:

java
// 场景1:只缓存VIP用户
@Cacheable(value = "user", condition = "#user.vip")

// 场景2:null结果不缓存(防缓存穿透的话用布隆过滤器更好,这里是防误缓存空值)
@Cacheable(value = "user", unless = "#result == null")

// 场景3:两者组合——只对VIP用户缓存,且结果不为空才缓存
@Cacheable(value = "user", condition = "#user.vip", unless = "#result == null")

面试关键:condition是"要不要参与缓存逻辑",unless是"结果要不要存缓存"。两者可以同时使用,执行顺序是先判断condition → 执行方法 → 再判断unless。


【面试题】8. Redis AOF重写机制是什么?为什么AOF重写能减小文件体积?重写期间的新写入命令怎么处理? ​

【标准答案】 AOF重写原理: AOF是追加写日志,随着时间推移文件会越来越大(比如对同一个key执行了100次SET,AOF记录100条命令,但最终状态只需要最后一条)。AOF重写就是遍历当前Redis内存状态,生成一组最精简的命令来重建当前数据,从而大幅减小AOF体积。

重写流程:

  1. 主进程调用BGREWRITEAOF,fork出子进程。
  2. 子进程遍历内存,生成新的AOF文件(只包含最终状态的命令)。
  3. 重写期间,主进程继续处理请求。新写入的命令会同时追加到旧AOF缓冲区和重写缓冲区(AOF Rewrite Buffer)。
  4. 子进程完成重写后,主进程将重写缓冲区的命令追加到新AOF文件末尾。
  5. 用新AOF文件原子替换旧AOF文件。

为什么能减小体积:比如一个计数器INCR了10000次,AOF记录10000条命令,重写后只需要一条SET counter 10000。过期key、已删除key、被覆盖的key在重写时直接跳过。

面试加分点:提一下混合持久化——Redis 4.0+的aof-use-rdb-preamble yes,重写时前半段用RDB格式(紧凑),后半段用AOF格式(增量),兼顾恢复速度和数据安全。


【面试题】9. 用Redis实现分布式限流,滑动窗口算法如何用Lua脚本保证原子性?相比固定窗口有什么优势? ​

【标准答案】 滑动窗口Lua脚本核心逻辑:

lua
local key = KEYS[1]
local window = tonumber(ARGV[1])  -- 窗口时间(毫秒)
local limit = tonumber(ARGV[2])   -- 窗口内最大请求数
local now = tonumber(ARGV[3])     -- 当前时间戳

-- 1. 移除窗口外的旧记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 2. 统计当前窗口内的请求数
local count = redis.call('ZCARD', key)

-- 3. 未超限则加入新请求
if count < limit then
    redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
    redis.call('PEXPIRE', key, window)
    return 1  -- 允许
else
    return 0  -- 拒绝
end

用ZSet实现,score存时间戳,member用时间戳-随机数保证唯一。Lua脚本在Redis中原子执行,不会被其他命令打断。

相比固定窗口的优势:

  • 固定窗口的致命问题——临界点突发:窗口结束前1秒涌入100个请求,新窗口开始后1秒又涌入100个请求,两个窗口各自未超限,但2秒内实际200个请求,远超预期。
  • 滑动窗口始终统计"最近N秒"的请求数,不存在临界点突变问题,限流更平滑。

面试加分:可以提一句生产中常用Redisson的RRateLimiter,内部就是基于滑动窗口+Lua实现的。


【面试题】10. 缓存与数据库双写一致性中,"延迟双删"策略的原理是什么?它有什么不足?生产环境中你更推荐什么方案? ​

【标准答案】 延迟双删流程:

  1. 先删除缓存
  2. 再更新数据库
  3. 延迟N毫秒后,再删除一次缓存

为什么需要第二次删除:第1步删缓存后、第2步更新数据库前,如果有读请求查到数据库旧值并回填缓存,就会导致缓存和数据库不一致。第3步的延迟删除就是清理这个"脏缓存"。

延迟时间N怎么定:取一次读请求的耗时(数据库查询+写缓存),通常几百毫秒到1秒。

不足:

  1. 延迟时间难以精确——设短了可能还有脏请求,设长了不一致窗口变大。
  2. 第二次删除也可能失败——如果失败,脏缓存就永远留着了。
  3. 本质上是"尽力而为"的最终一致性,不保证强一致。

生产推荐方案:

  1. Canal监听binlog:数据库变更后,Canal捕获binlog事件,异步更新/删除缓存。解耦、可靠、不侵入业务代码。
  2. 消息队列保障:更新数据库后发MQ消息,消费者删除缓存,失败则重试。
  3. 如果一致性要求不高:Cache Aside模式(先更新数据库再删缓存)+ 短TTL兜底即可,不用延迟双删。

面试建议:延迟双删能说出原理但不推荐作为首选,能体现出你理解"为什么"而不只是"怎么做"。