Skip to content

Spring集成Redis面试题库 ​

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


【面试题】1. Redis的ZSet底层编码有哪几种?什么情况下会转换?在排行榜场景如何优化? ​

【标准答案】 ZSet底层有两种编码:ziplist和skiplist。

编码转换条件:

  • 元素数量 < 128 且所有元素长度 < 64字节,使用ziplist
  • 超过阈值自动转为skiplist + dict组合结构

排行榜优化实战:

  1. 控制榜单长度:只保留Top N,用ZREMRANGEBYRANK清理
  2. 避免大Key:按时间分片(日榜、周榜、月榜)
  3. 批量查询:用ZRANGE批量获取排名,减少RTT
  4. 分页优化:避免深分页,改用游标式查询(记录lastScore)
  5. 预热缓存:凌晨预计算冷门榜单

实际项目中,我会设置zset-max-ziplist-entries 128,排行榜超过1万条就按天分片。


【面试题】2. Redis RDB和AOF各自的优缺点是什么?生产环境如何选择持久化策略? ​

【标准答案】 RDB优点:

  • 文件紧凑,适合备份和灾备
  • 恢复速度快,直接加载内存
  • 对性能影响小,fork子进程完成

RDB缺点:

  • 可能丢失最后一次快照后的数据
  • fork大内存进程可能阻塞(Copy-On-Write)

AOF优点:

  • 数据安全性高,最多丢1秒数据(everysec)
  • 可读性强,支持误操作恢复

AOF缺点:

  • 文件体积大
  • 恢复速度慢于RDB
  • 写入性能有损耗

生产环境选择:

  1. 只开RDB:允许分钟级数据丢失,追求极致性能
  2. 只开AOF:数据敏感,不允许丢失
  3. 混合持久化(推荐):RDB做基线 + AOF增量,兼顾恢复速度和数据安全

我们的订单系统用混合持久化,配置aof-use-rdb-preamble yes,AOF重写时先写RDB格式。


【面试题】3. Spring Data Redis中RedisTemplate和StringRedisTemplate有什么区别?序列化如何选择? ​

【标准答案】 核心区别:

  • RedisTemplate默认用JDK序列化,Key和Value都会加类信息前缀
  • StringRedisTemplate是RedisTemplate<String, String>特化版,用String序列化

序列化选择实战:

  1. StringRedisSerializer:Key必须用,可读性强
  2. JdkSerializationRedisSerializer:不推荐,序列化后体积大、有安全风险
  3. Jackson2JsonRedisSerializer:需要指定类型,适合单一对象
  4. GenericJackson2JsonRedisSerializer:自动带@class类型信息,适合多态对象

生产配置建议:

java
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

我们的项目统一用StringRedisTemplate存字符串,复杂对象用GenericJackson2JsonRedisSerializer。


【面试题】4. Redis缓存穿透、缓存击穿、缓存雪崩分别是什么?如何解决? ​

【标准答案】 缓存穿透:查询不存在的Key,请求直接打到数据库。

解决方案:

  1. 布隆过滤器:提前过滤不存在的Key
  2. 空值缓存:缓存null值,TTL设短(5分钟)
  3. 参数校验:拦截非法请求

缓存击穿:热点Key过期瞬间,大量请求同时击穿到数据库。

解决方案:

  1. 互斥锁:SET NX抢锁,只让一个线程查库
  2. 逻辑过期:不设TTL,后台异步更新
  3. 热点数据永不过期:配合主动更新

缓存雪崩:大量Key同时过期,或Redis宕机。

解决方案:

  1. 随机TTL:基础时间 + 随机值,避免同时过期
  2. 多级缓存:本地缓存(Caffeine) + Redis
  3. 熔断降级:Hystrix/Sentinel限流
  4. Redis高可用:主从 + 哨兵 + Cluster

我们的促销系统用布隆过滤器防穿透,热点商品用逻辑过期防击穿,TTL加随机值防雪崩。


【面试题】5. 如何用Redis实现分布式锁?Redisson的看门狗机制是什么? ​

【标准答案】 基础实现(SET NX EX):

java
// 加锁
SET lock:key unique_value NX EX 30
// 释放锁(Lua保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
end

问题:

  • 业务超时,锁自动释放导致并发
  • 误删其他线程的锁

Redisson原理:

  1. 可重入锁:Hash结构存储线程ID和重入次数
  2. 看门狗续期:后台线程每10秒续期(默认30秒锁),防止业务未执行完锁过期
  3. Lua原子操作:加锁、解锁都是原子脚本

实战配置:

java
RLock lock = redisson.getLock("order:lock:123");
try {
    if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
        // 执行业务
    }
} finally {
    lock.unlock();
}

我们的订单系统用Redisson,超时时间设业务执行时间的3倍,看门狗自动续期。


【面试题】6. Redis主从复制的工作原理是什么?全量同步和增量同步分别发生在什么场景? ​

【标准答案】 主从复制流程:

  1. 从节点发送PSYNC命令
  2. 主节点执行bgsave生成RDB,发送给从节点
  3. 主节点缓存期间写命令到复制积压缓冲区
  4. 从节点加载RDB,接收后续增量命令

全量同步触发场景:

  • 从节点首次连接
  • 从节点断开时间过长,复制积压缓冲区溢出
  • 主节点RunID变化(重启或切换)

增量同步触发场景:

  • 从节点短暂断开重连
  • 主节点仍保留复制积压缓冲区中的偏移量
  • 通过PSYNC <runid> <offset>请求部分同步

优化建议:

  1. 增大repl-backlog-size:默认1MB,生产建议100MB+
  2. 开启repl-diskless-sync:无盘复制,网络直传
  3. 合理控制从节点数量:避免主节点压力过大

我们的生产环境把复制缓冲区调到512MB,网络抖动时增量同步成功率达95%以上。


【面试题】7. Spring Cache注解@Cacheable的condition和unless有什么区别?如何实现动态TTL? ​

【标准答案】 condition vs unless:

  • condition:执行方法前判断,决定是否查缓存
  • unless:执行方法后判断,决定是否缓存结果(可以引用#result)

使用示例:

java
// condition:参数大于100才缓存
@Cacheable(value = "user", key = "#id", condition = "#id > 100")

// unless:结果为null不缓存
@Cacheable(value = "user", key = "#id", unless = "#result == null")

动态TTL实现: Spring Cache默认不支持动态TTL,需要自定义CacheManager:

java
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(30))
        .serializeKeysWith(RedisSerializationContext.SerializationPair
            .fromSerializer(new StringRedisSerializer()))
        .serializeValuesWith(RedisSerializationContext.SerializationPair
            .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    
    // 不同缓存空间不同TTL
    Map<String, RedisCacheConfiguration> cacheConfig = new HashMap<>();
    cacheConfig.put("user", config.entryTtl(Duration.ofHours(2)));
    cacheConfig.put("product", config.entryTtl(Duration.ofMinutes(10)));
    
    return RedisCacheManager.builder(factory)
        .cacheDefaults(config)
        .withInitialCacheConfigurations(cacheConfig)
        .build();
}

我们的系统按业务配置TTL:用户缓存2小时,商品缓存10分钟,验证码缓存5分钟。


【面试题】8. 如何用Redis实现接口限流?滑动窗口和令牌桶算法怎么选? ​

【标准答案】 滑动窗口实现(Lua脚本):

lua
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
local count = redis.call('ZCARD', key)
if count < limit then
    redis.call('ZADD', key, now, now)
    redis.call('PEXPIRE', key, window * 1000)
    return 1
end
return 0

令牌桶实现(Redisson):

java
RRateLimiter limiter = redisson.getRateLimiter("api:limit");
limiter.trySetRate(RateType.OVERALL, 100, 1, TimeUnit.SECONDS);
if (limiter.tryAcquire()) {
    // 允许访问
}

算法选择:

  • 滑动窗口:精确控制,适合严格限流(防止刷接口)
  • 令牌桶:允许突发流量,适合平滑限流(保护下游服务)
  • 漏桶:恒定速率流出,适合流量整形

生产建议:

  1. 网关层限流:Nginx + Lua,滑动窗口
  2. 应用层限流:Redisson RateLimiter,令牌桶
  3. 分布式限流:Redis集群 + Lua原子操作

我们的秒杀接口用滑动窗口严格限流,普通API用令牌桶允许一定突发。


【面试题】9. Redis Cluster为什么用16384个槽位而不是更多?节点间如何通信? ​

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

  1. 心跳包大小:每个槽位用1bit表示,16384槽位 = 2KB心跳包;如果65536槽位 = 8KB,网络开销大
  2. 主节点数量限制:建议最多1000个主节点,16384足够
  3. 槽数据结构:每个节点维护16384位的bitmap,空间效率高

节点通信机制(Gossip协议):

  1. 每个节点定时(每秒)随机选择几个节点发送PING
  2. PING消息包含:节点ID、槽位信息、故障检测状态
  3. 接收节点回复PONG,更新集群状态
  4. 如果节点故障,其他节点标记为PFAIL(主观下线)
  5. 多数主节点确认后,标记为FAIL(客观下线),触发故障转移

故障转移流程:

  1. 从节点发现主节点下线
  2. 从节点发起选举(Raft协议)
  3. 获得多数主节点投票后晋升为新主节点
  4. 新主节点广播槽位映射更新

我们的生产集群6主6从,心跳包2KB,集群状态同步延迟在100ms内。


【面试题】10. 如何用Redis实现延迟队列?Redisson延迟队列底层原理是什么? ​

【标准答案】 ZSet实现延迟队列:

java
// 生产者:订单30分钟后过期
long expireTime = System.currentTimeMillis() + 30 * 60 * 1000;
redis.zadd("delay:queue", expireTime, "order:123");

// 消费者:定时轮询
while (true) {
    Set<Tuple> tasks = redis.zrangebyscore("delay:queue", 0, 
        System.currentTimeMillis(), 0, 10);
    for (Tuple task : tasks) {
        // Lua原子操作移除并处理
        if (redis.zrem("delay:queue", task.getElement()) > 0) {
            processTask(task.getElement());
        }
    }
    Thread.sleep(1000);
}

Redisson延迟队列原理:

  1. 使用ZSet存储延迟任务(score为执行时间戳)
  2. 后台线程每100ms轮询到期任务
  3. 到期任务转移到就绪队列(List结构)
  4. 消费者从就绪队列BLPOP阻塞获取

Redisson使用示例:

java
RBlockingQueue<String> queue = redisson.getBlockingQueue("delay:order");
RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(queue);
delayedQueue.offer("order:123", 30, TimeUnit.MINUTES);

// 消费者
while (true) {
    String task = queue.take();
    processTask(task);
}

对比选择:

  • ZSet + 定时轮询:轻量,适合小规模
  • Redisson延迟队列:封装完善,支持持久化,生产推荐
  • Redis Stream:支持消费者组,适合消息可靠性要求高的场景

我们的订单超时取消用Redisson延迟队列,每天处理10万+延迟任务,零丢失。


总结:本轮覆盖数据结构、持久化、序列化、缓存三灾、分布式锁、主从复制、Spring Cache、限流、Cluster、延迟队列10大核心考点。每题都从原理到实战,面试官最爱问,回答时有理有据。