Skip to content

Spring集成Redis面试题库 ​

生成时间: 2026-05-22 22:29
题目数量: 10道


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

【面试题】1. Redis的String类型底层编码有哪几种?什么情况下会从embstr转换成raw? ​

【标准答案】String底层有三种编码:int、embstr、raw。

转换条件:

  • int:值为整数且范围在long内(-2^63到2^63-1)
  • embstr:值长度≤44字节(Redis 5.0后为44字节,之前是39字节),embstr是嵌入式字符串,redisObject和sdshdr分配在一块连续内存,一次分配,紧凑高效
  • raw:值长度>44字节,使用独立SDS(Simple Dynamic String),需要两次内存分配

什么时候转换:

  • int值被修改为字符串时,int→raw/embstr
  • embstr做任何修改操作(如APPEND),embstr→raw(因为embstr是只读的,修改需要重新分配)

面试加分点:embstr设计理念是减少内存碎片、提高缓存命中率。对于短字符串,embstr性能优于raw。


【面试题】2. Redis持久化中,AOF的everysec策略一定能保证最多丢失1秒数据吗? ​

【标准答案】不能保证,这个"everysec"有坑。

实际行为:

  • everysec模式下,AOF写入由后台线程每秒执行一次fsync
  • 如果系统崩溃,最多丢失最近一次fsync到崩溃时刻的数据,通常是1秒内
  • 但在高负载场景下,后台线程可能阻塞,导致多次数据未落盘,极端情况下可能丢失2秒甚至更多数据

三种策略对比:

  • always:每次写都fsync,最安全但性能最差(QPS下降一个数量级)
  • everysec:每秒fsync一次,性能和安全性的平衡点(生产推荐)
  • no:由OS决定刷盘,性能最好但可能丢失30秒数据

面试加分点:Redis 7.0引入了multi-part AOF,将AOF拆分为基础文件和增量文件,重写效率更高,恢复更快。实际生产中建议AOF+RDB混合持久化。


【面试题】3. Redis Cluster为什么设计成16384个槽位而不是65536或其他数字? ​

【标准答案】这是一个经典的架构设计权衡问题。

核心原因:

  1. 槽位信息交换效率:集群节点间通过Gossip协议交换信息,每个节点需要维护所有槽位映射表。16384个槽位,每个槽位用2字节表示,总共32KB。如果是65536个槽位,需要128KB,网络交换成本翻倍
  2. 实际集群规模:Redis官方建议集群节点数≤1000个,16384个槽位已足够支撑。每个节点平均管理16个槽位
  3. CRC16取模:16384 = 2^14,CRC16校验和是16位,取低14位正好映射到0-16383

槽位分配逻辑:

  • CRC16(key) % 16384 计算key所属槽位
  • MOVED重定向、ASK重定向都基于槽位号

面试加分点:槽位可以在线迁移(CLUSTER SETSLOT),迁移过程中支持ASK重定向,客户端可以平滑访问数据。


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

【标准答案】两者核心区别在于序列化策略。

区别对比:

维度RedisTemplateStringRedisTemplate
Key序列化默认JdkSerializationRedisSerializerStringRedisSerializer
Value序列化默认JdkSerializationRedisSerializerStringRedisSerializer
存储格式二进制(类名+对象数据)纯字符串(UTF-8)
可读性Redis客户端无法直接读取可读,方便调试
跨语言兼容差(Java专属)好(任何语言都能解析)

序列化器选择建议:

  • StringRedisSerializer:Key必须用,保证key可读性
  • GenericJackson2JsonRedisSerializer:Value推荐,存储JSON,跨语言兼容,但会多存一个@class字段
  • Jackson2JsonRedisSerializer:需要指定具体类型,不存储@class,更省空间
  • JdkSerializationRedisSerializer:不推荐,只能Java用,序列化后体积大

最佳实践配置:

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

面试加分点:StringRedisTemplate继承自RedisTemplate<String, String>,适合简单字符串场景。复杂对象存储建议自定义RedisTemplate配置。


【面试题】5. @Cacheable注解的condition和unless参数有什么区别?能否同时使用? ​

【标准答案】两者控制缓存的时机不同,可以同时使用。

区别对比:

参数作用时机语义示例
condition方法执行前判断true才查缓存/存缓存condition="#id > 0"
unless方法执行后判断true则不存缓存unless="#result == null"

执行流程:

  1. 先评估condition,false直接执行方法,不查缓存
  2. condition=true,查缓存
  3. 缓存未命中,执行方法
  4. 执行完后评估unless,true则不存缓存

典型场景:

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

// 场景2:不缓存null结果
@Cacheable(value = "user", unless = "#result == null")

// 场景3:组合使用 - 只缓存有效ID且结果非空的数据
@Cacheable(value = "user", condition = "#id > 0", unless = "#result == null")

面试加分点:unless可以访问返回值#result,condition不能访问返回值(方法还没执行)。unless还常用于过滤敏感数据,如unless="#result.sensitive"。


【面试题】6. 缓存穿透、缓存击穿、缓存雪崩分别是什么?解决方案有何不同? ​

【标准答案】这是Redis三大经典问题,本质都是缓存失效导致数据库压力激增。

问题对比:

问题现象原因解决方案
缓存穿透大量请求查询不存在的数据恶意攻击或业务缺陷布隆过滤器、空值缓存
缓存击穿某个热点key过期,瞬间大量请求打垮数据库热点key过期+高并发互斥锁、逻辑过期
缓存雪崩大量key同时过期或Redis宕机批量过期/Redis故障随机TTL、多级缓存、熔断降级

详细解决方案:

缓存穿透:

  • 布隆过滤器:bloom.add(expectedKey) 预加载所有合法key,请求前先用bloom判断
  • 空值缓存:查询返回null也缓存,设置短TTL(如2分钟)
  • 参数校验:接口层过滤非法参数

缓存击穿:

  • 互斥锁方案:SETNX lock_key 1 抢锁重建缓存,其他线程等待或返回旧值
  • 逻辑过期:缓存永不过期,数据中存过期时间,后台异步更新
  • Spring Boot集成:使用@Cacheable(sync = true),底层加同步锁

缓存雪崩:

  • 随机TTL:TTL = base + random(0, 300)
  • 多级缓存:本地缓存(Caffeine)+ Redis
  • 熔断降级:Hystrix/Sentinel,Redis不可用时返回降级数据

面试加分点:Redis Cluster模式下,某个节点宕机也会触发雪崩,需要配合哨兵或Cluster自动故障转移。生产环境建议多级缓存+熔断双保险。


【面试题】7. Redisson分布式锁的"看门狗"机制是什么原理?为什么要用Lua脚本释放锁? ​

【标准答案】看门狗机制解决的是锁超时问题,Lua脚本解决的是原子性问题。

看门狗机制:

问题背景:

  • 业务执行时间超过锁过期时间,锁自动释放,其他线程获取锁导致并发问题
  • 业务还没执行完,锁就被误删

看门狗原理:

  1. Redisson加锁成功后,启动一个后台线程(看门狗)
  2. 默认锁过期时间30秒,看门狗每10秒(过期时间/3)检查一次
  3. 如果持有锁的线程还在执行,自动续期30秒
  4. 业务执行完成后,主动释放锁,看门狗停止

核心代码:

java
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认看门狗生效
// 等价于 lock.lock(-1, TimeUnit.SECONDS) 看门狗生效

为什么要Lua释放锁:

释放锁需要三步原子操作:

  1. 判断锁是否属于当前线程
  2. 删除锁
  3. 从重入计数器减一

Lua脚本:

lua
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

不用Lua的问题:

  • 线程A执行GET发现是自己的锁
  • 线程A的锁刚好过期自动释放
  • 线程B抢到锁
  • 线程A执行DEL,把B的锁删了 ← 误删!

面试加分点:Redisson还支持可重入锁(hash结构存储重入次数)、读写锁(读读共享、写写互斥)、红锁RedLock(多节点分布式锁,但争议较大,生产慎用)。


【面试题】8. Redis Stream相比传统List+Pub/Sub实现消息队列有什么优势? ​

【标准答案】Stream是Redis 5.0引入的专门为消息队列设计的数据结构,解决了List和Pub/Sub的痛点。

三种方案对比:

维度ListPub/SubStream
消息持久化支持(BRPOP)不支持支持
消息确认不支持(消费即删除)不支持支持(XACK)
消费者组不支持不支持支持
消息回溯不支持不支持支持
消息顺序FIFOFIFOFIFO + 全局有序ID
离线消息不支持不支持支持

Stream核心能力:

  1. 消费者组:多个消费者共享一个Stream,每条消息只被消费一次

    bash
    XGROUP CREATE mystream mygroup $
    XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
    XACK mystream mygroup 1526569495631-0
  2. 消息确认机制:消费成功后XACK确认,失败可以XPENDING查看未确认消息,重新投递

  3. 消息回溯:可以从任意ID开始读取历史消息

    bash
    XREAD COUNT 10 STREAMS mystream 0
  4. 消息ID设计:时间戳-序号,全局有序,精度到毫秒级

Spring Boot集成:

java
@Bean
public StreamMessageListenerContainer<String, MapRecord<String, String, String>> 
    streamMessageListenerContainer(RedisConnectionFactory factory) {
    StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
        StreamMessageListenerContainerOptions.builder()
            .pollTimeout(Duration.ofSeconds(1))
            .build();
    return StreamMessageListenerContainer.create(factory, options);
}

面试加分点:Stream还支持Pending Entries List(PEL),未确认的消息会留在PEL中,可以XPENDING查看、XCLAIM重新分配给其他消费者。这解决了消费者宕机导致消息丢失的问题。


【面试题】9. 如何用Redis实现一个滑动窗口限流?Lua脚本如何保证原子性? ​

【标准答案】滑动窗口限流比固定窗口更平滑,避免窗口边界的突发流量。

算法原理:

  • 记录每个请求的时间戳(ZSet,score为时间戳)
  • 每次请求时,删除窗口外的旧请求,统计窗口内请求数
  • 如果请求数<阈值,允许访问;否则拒绝

Lua脚本实现:

lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])  -- 窗口大小(秒)
local now = tonumber(ARGV[3])

-- 1. 删除窗口外的旧请求
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)

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

-- 3. 判断是否超过阈值
if count < limit then
    redis.call('ZADD', key, now, now)  -- value也用时间戳(保证唯一)
    redis.call('EXPIRE', key, window)
    return 1  -- 允许访问
else
    return 0  -- 拒绝访问
end

Spring Boot集成:

java
public boolean isAllowed(String key, int limit, int windowSeconds) {
    long now = System.currentTimeMillis();
    DefaultRedisScript<Long> script = new DefaultRedisScript<>();
    script.setScriptSource(new ResourceScriptSource(
        new ClassPathResource("rate_limiter.lua")));
    script.setResultType(Long.class);
    
    Long result = redisTemplate.execute(script, 
        Collections.singletonList(key), 
        String.valueOf(limit), 
        String.valueOf(windowSeconds),
        String.valueOf(now));
    return result != null && result == 1;
}

为什么用Lua:

  • 删除旧请求 + 统计数量 + 添加新请求必须原子执行
  • 不用Lua的话,高并发下可能出现统计不准(刚删除完,又有新请求进来)

面试加分点:滑动窗口的时间复杂度是O(log N),因为ZREMRANGEBYSCORE和ZCARD都是O(log N)。对于大流量场景,可以用令牌桶算法(Redisson RateLimiter已实现),或者分层限流:单机Guava RateLimiter + 分布式Redis限流。


【面试题】10. Redis 7.x的Function特性相比Lua脚本有什么改进? ​

【标准答案】Function是Redis 7.0引入的新特性,被称为"Lua脚本2.0",解决了Lua脚本的多个痛点。

Function vs Lua对比:

维度Lua脚本Function
脚本管理散落在各处集中式库管理
脚本持久化不持久化(只存SHA)持久化到RDB/AOF
调试体验差(无断点)支持FUNCTION DEBUG
代码复用每次传完整脚本注册后直接调用
执行方式EVAL/EVALSHAFCALL
权限控制无ACL控制(Redis 7.0+)

Function核心用法:

lua
-- 注册函数库
FUNCTION LOAD "#!lua name=mylib\nredis.register_function('myfunc', function(keys, args) return redis.call('GET', keys[1]) end)"

-- 调用函数
FCALL myfunc 1 mykey

-- 查看所有函数
FUNCTION LIST

-- 删除函数库
FUNCTION DELETE mylib

Spring Boot集成:

java
// 注册函数
String functionCode = "redis.register_function('stock_decr', function(keys, args) " +
    "local stock = redis.call('GET', keys[1]) " +
    "if tonumber(stock) > 0 then " +
    "  redis.call('DECR', keys[1]) " +
    "  return 1 " +
    "end " +
    "return 0 " +
    "end)";

redisTemplate.execute("FUNCTION LOAD", 0, functionCode);

// 调用函数
redisTemplate.execute("FCALL", 1, "stock_decr", "product:1001");

Function的优势:

  1. 持久化:函数库会随RDB/AOF持久化,重启后不用重新注册
  2. 版本管理:FUNCTION LIST可以查看所有注册的函数,方便运维
  3. 调试友好:FUNCTION DEBUG支持单步调试,排查问题更高效
  4. 安全性:配合ACL限制函数执行权限

面试加分点:Function底层仍然用Lua引擎,但提供了更好的工程化支持。对于复杂的业务逻辑(如秒杀扣库存),建议用Function替代裸Lua脚本。不过Function也有学习成本,团队需要统一脚本管理规范。


本轮题目覆盖:数据结构底层编码、持久化策略、集群架构、序列化选择、缓存注解、缓存三大问题、分布式锁原理、消息队列演进、限流算法、Redis 7.x新特性。

难度分布:基础3题、进阶4题、高级3题。

适合场景:中高级Java开发面试、Spring Boot后端岗位、Redis专项面试。