Spring集成Redis面试题库
生成时间:2026-05-24 21:59
【面试题】1. Redis的ZSet底层编码有哪几种?什么情况下会转换?在排行榜场景如何优化?
【标准答案】 ZSet底层有两种编码:ziplist和skiplist。
编码转换条件:
- 元素数量 < 128 且所有元素长度 < 64字节,使用ziplist
- 超过阈值自动转为skiplist + dict组合结构
排行榜优化实战:
- 控制榜单长度:只保留Top N,用ZREMRANGEBYRANK清理
- 避免大Key:按时间分片(日榜、周榜、月榜)
- 批量查询:用ZRANGE批量获取排名,减少RTT
- 分页优化:避免深分页,改用游标式查询(记录lastScore)
- 预热缓存:凌晨预计算冷门榜单
实际项目中,我会设置zset-max-ziplist-entries 128,排行榜超过1万条就按天分片。
【面试题】2. Redis RDB和AOF各自的优缺点是什么?生产环境如何选择持久化策略?
【标准答案】 RDB优点:
- 文件紧凑,适合备份和灾备
- 恢复速度快,直接加载内存
- 对性能影响小,fork子进程完成
RDB缺点:
- 可能丢失最后一次快照后的数据
- fork大内存进程可能阻塞(Copy-On-Write)
AOF优点:
- 数据安全性高,最多丢1秒数据(everysec)
- 可读性强,支持误操作恢复
AOF缺点:
- 文件体积大
- 恢复速度慢于RDB
- 写入性能有损耗
生产环境选择:
- 只开RDB:允许分钟级数据丢失,追求极致性能
- 只开AOF:数据敏感,不允许丢失
- 混合持久化(推荐):RDB做基线 + AOF增量,兼顾恢复速度和数据安全
我们的订单系统用混合持久化,配置aof-use-rdb-preamble yes,AOF重写时先写RDB格式。
【面试题】3. Spring Data Redis中RedisTemplate和StringRedisTemplate有什么区别?序列化如何选择?
【标准答案】 核心区别:
- RedisTemplate默认用JDK序列化,Key和Value都会加类信息前缀
- StringRedisTemplate是RedisTemplate<String, String>特化版,用String序列化
序列化选择实战:
- StringRedisSerializer:Key必须用,可读性强
- JdkSerializationRedisSerializer:不推荐,序列化后体积大、有安全风险
- Jackson2JsonRedisSerializer:需要指定类型,适合单一对象
- GenericJackson2JsonRedisSerializer:自动带@class类型信息,适合多态对象
生产配置建议:
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,请求直接打到数据库。
解决方案:
- 布隆过滤器:提前过滤不存在的Key
- 空值缓存:缓存null值,TTL设短(5分钟)
- 参数校验:拦截非法请求
缓存击穿:热点Key过期瞬间,大量请求同时击穿到数据库。
解决方案:
- 互斥锁:SET NX抢锁,只让一个线程查库
- 逻辑过期:不设TTL,后台异步更新
- 热点数据永不过期:配合主动更新
缓存雪崩:大量Key同时过期,或Redis宕机。
解决方案:
- 随机TTL:基础时间 + 随机值,避免同时过期
- 多级缓存:本地缓存(Caffeine) + Redis
- 熔断降级:Hystrix/Sentinel限流
- Redis高可用:主从 + 哨兵 + Cluster
我们的促销系统用布隆过滤器防穿透,热点商品用逻辑过期防击穿,TTL加随机值防雪崩。
【面试题】5. 如何用Redis实现分布式锁?Redisson的看门狗机制是什么?
【标准答案】 基础实现(SET NX EX):
// 加锁
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原理:
- 可重入锁:Hash结构存储线程ID和重入次数
- 看门狗续期:后台线程每10秒续期(默认30秒锁),防止业务未执行完锁过期
- Lua原子操作:加锁、解锁都是原子脚本
实战配置:
RLock lock = redisson.getLock("order:lock:123");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 执行业务
}
} finally {
lock.unlock();
}我们的订单系统用Redisson,超时时间设业务执行时间的3倍,看门狗自动续期。
【面试题】6. Redis主从复制的工作原理是什么?全量同步和增量同步分别发生在什么场景?
【标准答案】 主从复制流程:
- 从节点发送PSYNC命令
- 主节点执行bgsave生成RDB,发送给从节点
- 主节点缓存期间写命令到复制积压缓冲区
- 从节点加载RDB,接收后续增量命令
全量同步触发场景:
- 从节点首次连接
- 从节点断开时间过长,复制积压缓冲区溢出
- 主节点RunID变化(重启或切换)
增量同步触发场景:
- 从节点短暂断开重连
- 主节点仍保留复制积压缓冲区中的偏移量
- 通过PSYNC <runid> <offset>请求部分同步
优化建议:
- 增大
repl-backlog-size:默认1MB,生产建议100MB+ - 开启
repl-diskless-sync:无盘复制,网络直传 - 合理控制从节点数量:避免主节点压力过大
我们的生产环境把复制缓冲区调到512MB,网络抖动时增量同步成功率达95%以上。
【面试题】7. Spring Cache注解@Cacheable的condition和unless有什么区别?如何实现动态TTL?
【标准答案】 condition vs unless:
- condition:执行方法前判断,决定是否查缓存
- unless:执行方法后判断,决定是否缓存结果(可以引用#result)
使用示例:
// 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:
@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脚本):
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):
RRateLimiter limiter = redisson.getRateLimiter("api:limit");
limiter.trySetRate(RateType.OVERALL, 100, 1, TimeUnit.SECONDS);
if (limiter.tryAcquire()) {
// 允许访问
}算法选择:
- 滑动窗口:精确控制,适合严格限流(防止刷接口)
- 令牌桶:允许突发流量,适合平滑限流(保护下游服务)
- 漏桶:恒定速率流出,适合流量整形
生产建议:
- 网关层限流:Nginx + Lua,滑动窗口
- 应用层限流:Redisson RateLimiter,令牌桶
- 分布式限流:Redis集群 + Lua原子操作
我们的秒杀接口用滑动窗口严格限流,普通API用令牌桶允许一定突发。
【面试题】9. Redis Cluster为什么用16384个槽位而不是更多?节点间如何通信?
【标准答案】 16384槽位原因:
- 心跳包大小:每个槽位用1bit表示,16384槽位 = 2KB心跳包;如果65536槽位 = 8KB,网络开销大
- 主节点数量限制:建议最多1000个主节点,16384足够
- 槽数据结构:每个节点维护16384位的bitmap,空间效率高
节点通信机制(Gossip协议):
- 每个节点定时(每秒)随机选择几个节点发送PING
- PING消息包含:节点ID、槽位信息、故障检测状态
- 接收节点回复PONG,更新集群状态
- 如果节点故障,其他节点标记为PFAIL(主观下线)
- 多数主节点确认后,标记为FAIL(客观下线),触发故障转移
故障转移流程:
- 从节点发现主节点下线
- 从节点发起选举(Raft协议)
- 获得多数主节点投票后晋升为新主节点
- 新主节点广播槽位映射更新
我们的生产集群6主6从,心跳包2KB,集群状态同步延迟在100ms内。
【面试题】10. 如何用Redis实现延迟队列?Redisson延迟队列底层原理是什么?
【标准答案】 ZSet实现延迟队列:
// 生产者:订单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延迟队列原理:
- 使用ZSet存储延迟任务(score为执行时间戳)
- 后台线程每100ms轮询到期任务
- 到期任务转移到就绪队列(List结构)
- 消费者从就绪队列BLPOP阻塞获取
Redisson使用示例:
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大核心考点。每题都从原理到实战,面试官最爱问,回答时有理有据。