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或其他数字?
【标准答案】这是一个经典的架构设计权衡问题。
核心原因:
- 槽位信息交换效率:集群节点间通过Gossip协议交换信息,每个节点需要维护所有槽位映射表。16384个槽位,每个槽位用2字节表示,总共32KB。如果是65536个槽位,需要128KB,网络交换成本翻倍
- 实际集群规模:Redis官方建议集群节点数≤1000个,16384个槽位已足够支撑。每个节点平均管理16个槽位
- CRC16取模:16384 = 2^14,CRC16校验和是16位,取低14位正好映射到0-16383
槽位分配逻辑:
CRC16(key) % 16384计算key所属槽位- MOVED重定向、ASK重定向都基于槽位号
面试加分点:槽位可以在线迁移(CLUSTER SETSLOT),迁移过程中支持ASK重定向,客户端可以平滑访问数据。
【面试题】4. Spring Data Redis中,RedisTemplate和StringRedisTemplate有什么区别?序列化器如何选择?
【标准答案】两者核心区别在于序列化策略。
区别对比:
| 维度 | RedisTemplate | StringRedisTemplate |
|---|---|---|
| Key序列化 | 默认JdkSerializationRedisSerializer | StringRedisSerializer |
| Value序列化 | 默认JdkSerializationRedisSerializer | StringRedisSerializer |
| 存储格式 | 二进制(类名+对象数据) | 纯字符串(UTF-8) |
| 可读性 | Redis客户端无法直接读取 | 可读,方便调试 |
| 跨语言兼容 | 差(Java专属) | 好(任何语言都能解析) |
序列化器选择建议:
- StringRedisSerializer:Key必须用,保证key可读性
- GenericJackson2JsonRedisSerializer:Value推荐,存储JSON,跨语言兼容,但会多存一个@class字段
- Jackson2JsonRedisSerializer:需要指定具体类型,不存储@class,更省空间
- JdkSerializationRedisSerializer:不推荐,只能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" |
执行流程:
- 先评估condition,false直接执行方法,不查缓存
- condition=true,查缓存
- 缓存未命中,执行方法
- 执行完后评估unless,true则不存缓存
典型场景:
// 场景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脚本解决的是原子性问题。
看门狗机制:
问题背景:
- 业务执行时间超过锁过期时间,锁自动释放,其他线程获取锁导致并发问题
- 业务还没执行完,锁就被误删
看门狗原理:
- Redisson加锁成功后,启动一个后台线程(看门狗)
- 默认锁过期时间30秒,看门狗每10秒(过期时间/3)检查一次
- 如果持有锁的线程还在执行,自动续期30秒
- 业务执行完成后,主动释放锁,看门狗停止
核心代码:
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认看门狗生效
// 等价于 lock.lock(-1, TimeUnit.SECONDS) 看门狗生效为什么要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的痛点。
三种方案对比:
| 维度 | List | Pub/Sub | Stream |
|---|---|---|---|
| 消息持久化 | 支持(BRPOP) | 不支持 | 支持 |
| 消息确认 | 不支持(消费即删除) | 不支持 | 支持(XACK) |
| 消费者组 | 不支持 | 不支持 | 支持 |
| 消息回溯 | 不支持 | 不支持 | 支持 |
| 消息顺序 | FIFO | FIFO | FIFO + 全局有序ID |
| 离线消息 | 不支持 | 不支持 | 支持 |
Stream核心能力:
消费者组:多个消费者共享一个Stream,每条消息只被消费一次
bashXGROUP CREATE mystream mygroup $ XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream > XACK mystream mygroup 1526569495631-0消息确认机制:消费成功后XACK确认,失败可以XPENDING查看未确认消息,重新投递
消息回溯:可以从任意ID开始读取历史消息
bashXREAD COUNT 10 STREAMS mystream 0消息ID设计:
时间戳-序号,全局有序,精度到毫秒级
Spring Boot集成:
@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脚本实现:
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 -- 拒绝访问
endSpring Boot集成:
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/EVALSHA | FCALL |
| 权限控制 | 无 | ACL控制(Redis 7.0+) |
Function核心用法:
-- 注册函数库
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 mylibSpring Boot集成:
// 注册函数
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的优势:
- 持久化:函数库会随RDB/AOF持久化,重启后不用重新注册
- 版本管理:
FUNCTION LIST可以查看所有注册的函数,方便运维 - 调试友好:
FUNCTION DEBUG支持单步调试,排查问题更高效 - 安全性:配合ACL限制函数执行权限
面试加分点:Function底层仍然用Lua引擎,但提供了更好的工程化支持。对于复杂的业务逻辑(如秒杀扣库存),建议用Function替代裸Lua脚本。不过Function也有学习成本,团队需要统一脚本管理规范。
本轮题目覆盖:数据结构底层编码、持久化策略、集群架构、序列化选择、缓存注解、缓存三大问题、分布式锁原理、消息队列演进、限流算法、Redis 7.x新特性。
难度分布:基础3题、进阶4题、高级3题。
适合场景:中高级Java开发面试、Spring Boot后端岗位、Redis专项面试。