Skip to content

Spring集成Redis面试题库 ​

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


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

【标准答案】String类型有三种底层编码:

  1. int:存储整数值且值在long范围内(-2^63 到 2^63-1)
  2. embstr:存储短字符串,长度小于等于44字节(Redis 5.0+是39字节)
  3. raw:存储长字符串,长度超过embstr阈值

编码转换触发条件:

  • int编码:当执行APPEND或INCRBYFLOAT等操作使值变为非整数,会转为raw编码
  • embstr编码:embstr是只读的,任何修改操作(APPEND、SETRANGE等)都会转为raw编码
  • embstr无法转回int,只能单向转换

实际开发中,如果要用String存JSON且频繁修改,建议直接用raw编码,避免embstr转raw的性能开销。


【面试题】2. AOF的fsync策略有哪三种?生产环境推荐用哪种? ​

【标准答案】三种fsync策略:

  1. always:每条写命令都fsync,最多丢失1条命令,性能最差
  2. everysec:每秒fsync一次,最多丢失1秒数据,性能和安全的平衡点
  3. 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更适合热点场景

实际配置示例:

bash
maxmemory 4gb
maxmemory-policy allkeys-lru

【面试题】4. Redis主从复制的psync2有什么改进?解决了什么问题? ​

【标准答案】psync2是Redis 4.0引入的改进版同步协议,主要改进:

核心改进:

  1. 支持部分重同步:从库断线重连时,如果主库的复制积压缓冲区还有数据,只需要同步增量数据
  2. 复制ID机制:主库有主从复制ID,从库记录主库ID,重连时用ID判断能否增量同步
  3. 复制积压缓冲区:环形缓冲区,默认1MB,可通过repl_backlog_size调整

解决的问题:

  • 老版本sync命令:每次断线重连都要全量同步,效率低
  • psync2:大部分情况只需增量同步,减少主库压力和网络带宽

实际配置建议:

bash
# 根据写流量调整积压缓冲区大小
repl_backlog_size 100mb

面试加分点:如果从库断线时间过长(超过复制积压缓冲区覆盖范围),仍然需要全量同步。生产环境建议把积压缓冲区设置大一些。


【面试题】5. Spring Cache的@Cacheable注解的condition和unless有什么区别? ​

【标准答案】两者都是条件缓存,但生效时机和作用不同:

condition(执行前判断):

  • 在方法执行前判断
  • 返回false则不查询缓存,直接执行方法,也不缓存结果
  • 用SpEL表达式,可以访问方法参数

unless(执行后判断):

  • 在方法执行后判断
  • 返回true则不缓存方法结果,但已经查询过缓存了
  • 可以访问方法返回值(#result)

代码示例:

java
// 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,导致数据库压力过大。

解决方案一:空值缓存

java
@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。

解决方案二:布隆过滤器(推荐)

java
@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秒
  • 业务执行完毕,主动释放锁并停止看门狗

代码示例:

java
RLock lock = redisson.getLock("order:lock:123");
try {
    // 不设置leaseTime,默认启用看门狗
    lock.lock();
    // 业务执行超过30秒也没关系,看门狗会续期
    processOrder();
} finally {
    lock.unlock();
}

关闭看门狗的情况:

java
// 手动设置过期时间,不启用看门狗
lock.lock(10, TimeUnit.SECONDS);

面试加分点:看门狗续期是用Lua脚本实现的,保证原子性;如果Redis宕机,锁会自动过期,避免死锁。


【面试题】8. Redis Stream消费者组的消息确认机制是怎样的?如何保证消息不丢失? ​

【标准答案】Redis Stream是Redis 5.0引入的消息队列结构,消费者组机制类似Kafka:

核心概念:

  • XADD:生产消息
  • XGROUP CREATE:创建消费者组
  • XREADGROUP:消费者读取消息
  • XACK:确认消息已处理
  • XPENDING:查看待确认消息

消息流转:

新消息 → 待消费(待分配) → 已分配未确认 → 已确认

保证不丢失的三层机制:

  1. 消息持久化:Stream消息存储在Redis,即使消费者宕机消息也不丢失
  2. Pending List(待确认列表):消费者读取消息后,消息进入Pending List,直到XACK确认
  3. 消息转移:消费者宕机后,其他消费者可以用XCLAIM转移其Pending消息

代码示例:

java
// 生产消息
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);

死信处理:

java
// 查询Pending消息,超过一定次数转为死信
PendingMessagesSummary pending = redisTemplate.opsForStream()
    .pending("order:stream", "order-group");

【面试题】9. 如何用Redis的ZSet实现滑动窗口限流?请给出Lua脚本实现。 ​

【标准答案】滑动窗口限流原理:统计最近N秒内的请求数量。

ZSet实现思路:

  • member:请求唯一ID或时间戳
  • score:请求时间戳(毫秒)
  • 每次请求时:删除窗口外的数据,统计窗口内数量,判断是否超限

Lua脚本(原子执行):

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  -- 限流
end

Spring Boot集成:

java
@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脚本保证库存扣减的原子性? ​

【标准答案】秒杀的核心问题:库存检查和扣减必须原子执行,防止超卖。

传统方案的问题:

java
// 错误示范:检查和扣减不是原子操作
int stock = redisTemplate.opsForValue().get("seckill:stock:123");
if (stock > 0) {
    redisTemplate.opsForValue().decrement("seckill:stock:123");
}
// 高并发下,多个线程同时检查通过,导致超卖

Lua脚本方案(原子执行):

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调用:

java
@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();
        }
    }
}

完整方案要点:

  1. 预热库存:秒杀开始前,将库存同步到Redis
  2. Lua原子扣减:所有库存操作都在Redis完成
  3. 异步落库:使用消息队列异步写入订单和扣减数据库库存
  4. 兜底方案:定时任务同步Redis和数据库库存

题目考点分布 ​

序号考点难度
1数据结构底层编码⭐⭐
2AOF fsync策略⭐⭐
3内存淘汰策略⭐⭐⭐
4主从复制psync2⭐⭐⭐
5Spring Cache条件缓存⭐⭐
6缓存穿透+布隆过滤器⭐⭐⭐⭐
7Redisson看门狗机制⭐⭐⭐⭐
8Stream消费者组⭐⭐⭐⭐
9滑动窗口限流⭐⭐⭐⭐⭐
10秒杀Lua原子操作⭐⭐⭐⭐⭐

本题库基于Redis 7.x版本,Spring Boot 3.x,持续更新中...