Spring集成Redis面试题库
生成时间:2026-05-21 22:51 题目数量:10道
【面试题】1. Redis String类型的底层编码有几种?什么情况下会触发编码转换?
【标准答案】String类型有三种底层编码:
- int:存储整数值且值在long范围内(-2^63 到 2^63-1)
- embstr:存储短字符串,长度小于等于44字节(Redis 5.0+是39字节)
- raw:存储长字符串,长度超过embstr阈值
编码转换触发条件:
- int编码:当执行APPEND或INCRBYFLOAT等操作使值变为非整数,会转为raw编码
- embstr编码:embstr是只读的,任何修改操作(APPEND、SETRANGE等)都会转为raw编码
- embstr无法转回int,只能单向转换
实际开发中,如果要用String存JSON且频繁修改,建议直接用raw编码,避免embstr转raw的性能开销。
【面试题】2. AOF的fsync策略有哪三种?生产环境推荐用哪种?
【标准答案】三种fsync策略:
- always:每条写命令都fsync,最多丢失1条命令,性能最差
- everysec:每秒fsync一次,最多丢失1秒数据,性能和安全的平衡点
- no:由操作系统决定何时fsync,可能丢失30秒数据,性能最好
生产环境推荐:
- 主库用everysec:兼顾性能和数据安全,最常用的配置
- 从库可以用no:从库数据可以从主库重新同步
- 如果对数据安全要求极高(金融场景),考虑用always,但要做好性能压测
补充说明:Redis 7.0+的混合持久化(RDB+AOF)默认开启,AOF重写时先写RDB格式的快照,再追加AOF增量,恢复速度更快。
【面试题】3. Redis内存淘汰策略有哪些?缓存场景推荐用哪种?
【标准答案】8种淘汰策略:
不淘汰类:
- noeviction:内存满时拒绝写入,返回错误
全量键类:
- allkeys-lru:从所有键中淘汰最久未使用的
- allkeys-lfu:从所有键中淘汰最少使用的
- allkeys-random:随机淘汰
带过期时间类:
- volatile-lru:从设置了过期时间的键中淘汰最久未使用的
- volatile-lfu:从设置了过期时间的键中淘汰最少使用的
- volatile-random:从设置了过期时间的键中随机淘汰
- volatile-ttl:淘汰即将过期的键(TTL最小的)
缓存场景推荐:
- 纯缓存场景用allkeys-lru:确保内存满时能继续写入,LRU算法更符合缓存访问模式
- 缓存+持久数据混合用volatile-lru:只淘汰缓存数据,保护持久数据
- 热点数据明显用allkeys-lfu:LFU能识别访问频率,比LRU更适合热点场景
实际配置示例:
maxmemory 4gb
maxmemory-policy allkeys-lru【面试题】4. Redis主从复制的psync2有什么改进?解决了什么问题?
【标准答案】psync2是Redis 4.0引入的改进版同步协议,主要改进:
核心改进:
- 支持部分重同步:从库断线重连时,如果主库的复制积压缓冲区还有数据,只需要同步增量数据
- 复制ID机制:主库有主从复制ID,从库记录主库ID,重连时用ID判断能否增量同步
- 复制积压缓冲区:环形缓冲区,默认1MB,可通过repl_backlog_size调整
解决的问题:
- 老版本sync命令:每次断线重连都要全量同步,效率低
- psync2:大部分情况只需增量同步,减少主库压力和网络带宽
实际配置建议:
# 根据写流量调整积压缓冲区大小
repl_backlog_size 100mb面试加分点:如果从库断线时间过长(超过复制积压缓冲区覆盖范围),仍然需要全量同步。生产环境建议把积压缓冲区设置大一些。
【面试题】5. Spring Cache的@Cacheable注解的condition和unless有什么区别?
【标准答案】两者都是条件缓存,但生效时机和作用不同:
condition(执行前判断):
- 在方法执行前判断
- 返回false则不查询缓存,直接执行方法,也不缓存结果
- 用SpEL表达式,可以访问方法参数
unless(执行后判断):
- 在方法执行后判断
- 返回true则不缓存方法结果,但已经查询过缓存了
- 可以访问方法返回值(#result)
代码示例:
// condition:id小于100才查缓存
@Cacheable(value = "user", key = "#id", condition = "#id < 100")
public User getUserById(Long id) { ... }
// unless:返回null不缓存
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUserById(Long id) { ... }
// 组合使用:id<100且返回非null才缓存
@Cacheable(value = "user", key = "#id",
condition = "#id < 100", unless = "#result == null")
public User getUserById(Long id) { ... }实战经验:unless常用于排除null值、空集合、失败结果等,避免缓存无效数据。
【面试题】6. 什么是缓存穿透?项目中如何用布隆过滤器解决?
【标准答案】缓存穿透:查询一个不存在的数据,缓存没有,数据库也没有,导致每次请求都打到数据库。
攻击场景:恶意请求大量不存在的key,导致数据库压力过大。
解决方案一:空值缓存
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
// 缓存空值,设置短过期时间
redisTemplate.opsForValue().set("user:null:" + id, "", 5, TimeUnit.MINUTES);
}
return user;
}缺点:占用内存,可能缓存大量无效key。
解决方案二:布隆过滤器(推荐)
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 初始化布隆过滤器(Redis 4.0+的RedisBloom模块)
public void initBloomFilter() {
// 加载所有有效用户ID到布隆过滤器
List<Long> userIds = userMapper.selectAllIds();
for (Long id : userIds) {
redisTemplate.opsForValue().setBit("user:bloom", hash(id), true);
}
}
public User getUserById(Long id) {
// 先查布隆过滤器
if (!bloomFilter.mightContain("user:bloom", id)) {
return null; // 一定不存在
}
// 可能存在,查缓存
// ...
}
}布隆过滤器特点:
- 判断不存在:100%准确
- 判断存在:有误判率(可配置),可能实际不存在
- 空间效率高:1%误判率只需要9.6bits/元素
【面试题】7. Redisson分布式锁的看门狗(Watchdog)机制是什么?解决了什么问题?
【标准答案】看门狗机制解决锁超时问题:
问题场景:
- 线程A获取锁,设置30秒过期
- 线程A执行业务逻辑超过30秒
- 锁自动过期释放,线程B获取锁
- 线程A执行完毕释放锁,把线程B的锁删了
看门狗原理:
- Redisson获取锁时不设置过期时间,默认30秒
- 后台启动一个定时任务(每10秒,即过期时间的1/3)
- 检查锁是否还持有,如果持有就续期到30秒
- 业务执行完毕,主动释放锁并停止看门狗
代码示例:
RLock lock = redisson.getLock("order:lock:123");
try {
// 不设置leaseTime,默认启用看门狗
lock.lock();
// 业务执行超过30秒也没关系,看门狗会续期
processOrder();
} finally {
lock.unlock();
}关闭看门狗的情况:
// 手动设置过期时间,不启用看门狗
lock.lock(10, TimeUnit.SECONDS);面试加分点:看门狗续期是用Lua脚本实现的,保证原子性;如果Redis宕机,锁会自动过期,避免死锁。
【面试题】8. Redis Stream消费者组的消息确认机制是怎样的?如何保证消息不丢失?
【标准答案】Redis Stream是Redis 5.0引入的消息队列结构,消费者组机制类似Kafka:
核心概念:
- XADD:生产消息
- XGROUP CREATE:创建消费者组
- XREADGROUP:消费者读取消息
- XACK:确认消息已处理
- XPENDING:查看待确认消息
消息流转:
新消息 → 待消费(待分配) → 已分配未确认 → 已确认保证不丢失的三层机制:
- 消息持久化:Stream消息存储在Redis,即使消费者宕机消息也不丢失
- Pending List(待确认列表):消费者读取消息后,消息进入Pending List,直到XACK确认
- 消息转移:消费者宕机后,其他消费者可以用XCLAIM转移其Pending消息
代码示例:
// 生产消息
redisTemplate.opsForStream().add(StreamRecords.newRecord()
.ofObject(payload)
.withStreamKey("order:stream"));
// 消费消息
List<MapRecord<String, Object, Object>> messages = redisTemplate.opsForStream()
.read(Consumer.from("order-group", "consumer-1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("order:stream", ReadOffset.lastConsumed()));
// 处理完确认
redisTemplate.opsForStream().acknowledge("order:stream", "order-group", messageId);死信处理:
// 查询Pending消息,超过一定次数转为死信
PendingMessagesSummary pending = redisTemplate.opsForStream()
.pending("order:stream", "order-group");【面试题】9. 如何用Redis的ZSet实现滑动窗口限流?请给出Lua脚本实现。
【标准答案】滑动窗口限流原理:统计最近N秒内的请求数量。
ZSet实现思路:
- member:请求唯一ID或时间戳
- score:请求时间戳(毫秒)
- 每次请求时:删除窗口外的数据,统计窗口内数量,判断是否超限
Lua脚本(原子执行):
local key = KEYS[1] -- 限流key
local limit = tonumber(ARGV[1]) -- 最大请求数
local window = tonumber(ARGV[2]) -- 窗口大小(毫秒)
local now = tonumber(ARGV[3]) -- 当前时间戳
-- 删除窗口外的数据
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 统计窗口内请求数量
local count = redis.call('ZCARD', key)
if count < limit then
-- 未超限,添加当前请求
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('PEXPIRE', key, window)
return 1 -- 允许通过
else
return 0 -- 限流
endSpring Boot集成:
@Service
public class RateLimitService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean allowRequest(String key, int limit, int windowMs) {
String script = "..."; // 上面Lua脚本
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
String.valueOf(limit),
String.valueOf(windowMs),
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1;
}
}高级特性:
- 令牌桶用Hash+Lua实现:存储令牌数量和最后更新时间
- 漏桶用List实现:固定速率消费请求
【面试题】10. 秒杀场景如何用Lua脚本保证库存扣减的原子性?
【标准答案】秒杀的核心问题:库存检查和扣减必须原子执行,防止超卖。
传统方案的问题:
// 错误示范:检查和扣减不是原子操作
int stock = redisTemplate.opsForValue().get("seckill:stock:123");
if (stock > 0) {
redisTemplate.opsForValue().decrement("seckill:stock:123");
}
// 高并发下,多个线程同时检查通过,导致超卖Lua脚本方案(原子执行):
local stockKey = KEYS[1]
local userId = ARGV[1]
local quantity = tonumber(ARGV[2])
-- 检查库存
local stock = tonumber(redis.call('GET', stockKey))
if not stock then
return -1 -- 商品不存在
end
if stock < quantity then
return 0 -- 库存不足
end
-- 扣减库存(原子操作)
redis.call('DECRBY', stockKey, quantity)
-- 记录购买记录(防重复购买)
local boughtKey = stockKey .. ':bought'
if redis.call('SISMEMBER', boughtKey, userId) == 1 then
return -2 -- 已经买过
end
redis.call('SADD', boughtKey, userId)
return 1 -- 扣减成功Spring Boot调用:
@Service
public class SeckillService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedisScript<Long> seckillScript;
public SeckillResult seckill(Long productId, Long userId, int quantity) {
Long result = redisTemplate.execute(
seckillScript,
Arrays.asList("seckill:stock:" + productId),
userId.toString(),
String.valueOf(quantity)
);
switch (result.intValue()) {
case 1: return SeckillResult.success();
case 0: return SeckillResult.stockNotEnough();
case -1: return SeckillResult.productNotFound();
case -2: return SeckillResult.alreadyBought();
default: return SeckillResult.failed();
}
}
}完整方案要点:
- 预热库存:秒杀开始前,将库存同步到Redis
- Lua原子扣减:所有库存操作都在Redis完成
- 异步落库:使用消息队列异步写入订单和扣减数据库库存
- 兜底方案:定时任务同步Redis和数据库库存
题目考点分布
| 序号 | 考点 | 难度 |
|---|---|---|
| 1 | 数据结构底层编码 | ⭐⭐ |
| 2 | AOF fsync策略 | ⭐⭐ |
| 3 | 内存淘汰策略 | ⭐⭐⭐ |
| 4 | 主从复制psync2 | ⭐⭐⭐ |
| 5 | Spring Cache条件缓存 | ⭐⭐ |
| 6 | 缓存穿透+布隆过滤器 | ⭐⭐⭐⭐ |
| 7 | Redisson看门狗机制 | ⭐⭐⭐⭐ |
| 8 | Stream消费者组 | ⭐⭐⭐⭐ |
| 9 | 滑动窗口限流 | ⭐⭐⭐⭐⭐ |
| 10 | 秒杀Lua原子操作 | ⭐⭐⭐⭐⭐ |
本题库基于Redis 7.x版本,Spring Boot 3.x,持续更新中...