本轮 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流程:
- 主进程调用
bgsave命令,Redisfork出子进程。fork瞬间主线程会短暂阻塞(复制页表,通常几毫秒到几十毫秒,取决于内存大小)。 - fork完成后,子进程开始遍历内存生成RDB文件,主进程继续正常处理请求。
- 子进程写RDB时,利用操作系统的COW(Copy-On-Write)机制:主进程若修改某页数据,OS会先复制该页给主进程,子进程仍然读旧页。所以子进程看到的是fork时刻的一致性快照。
- RDB写完后,子进程退出,主进程通过信号得知完成,用临时RDB替换旧RDB。
关键点:
- fork的阻塞时间与Redis内存大小成正比(页表越大,复制越慢),内存几十GB时可能阻塞上百毫秒,这是RDB的主要痛点。
- COW的代价是额外内存开销——修改的页会复制一份,如果写操作密集,内存可能翻倍。
- 生产环境建议用
bgsave而非save,save是主线程同步生成RDB,会完全阻塞。
【面试题】3. Spring Data Redis中Lettuce和Jedis有什么核心区别?为什么Spring Boot 2.x之后默认选择Lettuce?
【标准答案】 核心区别:
| 维度 | Jedis | Lettuce |
|---|---|---|
| 连接方式 | 直连,每个Jedis实例对应一个TCP连接 | 基于Netty的连接池,单个连接可复用 |
| 线程安全 | 不安全,多线程需用连接池(JedisPool) | 安全,单连接支持并发操作 |
| 异步 | 不支持 | 支持异步/响应式(Reactive) |
| 集群 | 手动管理分片连接 | 自动拓扑刷新和分片 |
| 性能 | 低并发下简单直接 | 高并发下性能更优(连接复用+管道) |
Spring Boot 2.x默认选Lettuce的原因:
- 连接复用:Lettuce基于Netty,一个TCP连接通过队列化命令支持多线程并发,资源利用率高。
- 天然支持响应式:配合WebFlux做响应式编程,Jedis做不到。
- 集群自适应:Redis Cluster节点变化时Lettuce能自动刷新拓扑,Jedis需要手动处理。
- 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秒。
看门狗机制:
- 加锁成功后,Redisson会启动一个定时任务(Watchdog),每隔
锁超时时间/3(即10秒)检查一次。 - 如果当前线程仍持有锁(通过Lua脚本判断value是否匹配),就自动续期到30秒。
- 如果持有锁的客户端宕机,看门狗自然停止,锁30秒后自动过期释放。
业务超30秒:只要客户端还活着且没有主动释放锁,看门狗会持续续期,锁不会过期。只有以下情况锁才会丢失:
- 客户端进程崩溃或重启 → 看门狗停止 → 30秒后锁过期
- 显式调用
lock.unlock()→ 立即释放 - 调用
lock.lock(timeout, TimeUnit)指定了超时时间 → 不会启动看门狗,到期强制释放
面试关键:指定了leaseTime就不会启动看门狗,这是最常见的踩坑点。如果业务时间不确定,不要传leaseTime参数。
【面试题】6. Redis Cluster为什么使用16384个槽位而不是65536?节点间通信用的Gossip协议有什么优缺点?
【标准答案】 16384槽位的原因:
- 心跳包大小:集群节点定期交换心跳包,携带自身负责的槽位信息。16384个槽用2KB的bitmap就能表示(16384/8=2048字节),而65536需要8KB。节点越多、心跳越频繁,差距越明显。
- 集群规模合理上限:Redis Cluster建议不超过1000个节点,16384个槽完全够用。每个节点至少负责一个槽,1000个节点平均16个槽/节点,足够均匀。
- 槽位迁移开销:槽迁移时需要逐个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"→ 结果集太大时不缓存
典型使用场景:
// 场景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体积。
重写流程:
- 主进程调用
BGREWRITEAOF,fork出子进程。 - 子进程遍历内存,生成新的AOF文件(只包含最终状态的命令)。
- 重写期间,主进程继续处理请求。新写入的命令会同时追加到旧AOF缓冲区和重写缓冲区(AOF Rewrite Buffer)。
- 子进程完成重写后,主进程将重写缓冲区的命令追加到新AOF文件末尾。
- 用新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脚本核心逻辑:
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. 缓存与数据库双写一致性中,"延迟双删"策略的原理是什么?它有什么不足?生产环境中你更推荐什么方案?
【标准答案】 延迟双删流程:
- 先删除缓存
- 再更新数据库
- 延迟N毫秒后,再删除一次缓存
为什么需要第二次删除:第1步删缓存后、第2步更新数据库前,如果有读请求查到数据库旧值并回填缓存,就会导致缓存和数据库不一致。第3步的延迟删除就是清理这个"脏缓存"。
延迟时间N怎么定:取一次读请求的耗时(数据库查询+写缓存),通常几百毫秒到1秒。
不足:
- 延迟时间难以精确——设短了可能还有脏请求,设长了不一致窗口变大。
- 第二次删除也可能失败——如果失败,脏缓存就永远留着了。
- 本质上是"尽力而为"的最终一致性,不保证强一致。
生产推荐方案:
- Canal监听binlog:数据库变更后,Canal捕获binlog事件,异步更新/删除缓存。解耦、可靠、不侵入业务代码。
- 消息队列保障:更新数据库后发MQ消息,消费者删除缓存,失败则重试。
- 如果一致性要求不高:Cache Aside模式(先更新数据库再删缓存)+ 短TTL兜底即可,不用延迟双删。
面试建议:延迟双删能说出原理但不推荐作为首选,能体现出你理解"为什么"而不只是"怎么做"。