面试被问 Redis?这 3 个问题 90% 的人都答不对

🔥 问题一:Redis 单线程为什么还能支撑 10w+ QPS?

面试官潜台词: 考察你对 Redis 高性能原理的理解深度。

90% 的人答错:

"因为 Redis 是纯内存操作,所以快。"

❌ 太浅了! 内存操作只是基础,单线程设计才是精髓。

正确答案:

scss 复制代码
// Redis 的核心设计:单线程 + IO多路复用
// 类比 Java NIO 的 Selector 机制

// 传统阻塞IO(低效)
while (true) {
    Socket socket = serverSocket.accept(); // 阻塞!
    handle(socket); // 处理完才能接受下一个
}

// Redis 的 IO多路复用(高效)
while (!stop) {
    // 1. 批量获取就绪的 socket(非阻塞)
    readySockets = epoll_wait(epfd, events, maxevents, timeout);
    
    // 2. 逐个处理(纯内存操作,极快)
    for (i = 0; i < readySockets; i++) {
        handle(events[i].data.fd);
    }
}

核心原因 3 点:

设计 作用
纯内存操作 纳秒级响应,比磁盘快 10 万倍
单线程模型 避免上下文切换、锁竞争,代码简单高效
IO多路复用 一个线程管理数万连接,epoll 性能炸裂

💡 一句话总结: Redis 把"单线程"做成了优势------没有锁、没有切换、没有竞争,把内存性能压榨到极致。


🔥 问题二:缓存穿透、击穿、雪崩,到底啥区别?

面试官潜台词: 考察你对缓存架构风险的理解和解决方案。

90% 的人混淆:

"都是缓存失效了,然后请求打到数据库..."

❌ 概念不清! 三者场景和解决方案完全不同。

一张图讲清楚:

rust 复制代码
缓存穿透          缓存击穿            缓存雪崩
   ↓                ↓                  ↓
查询不存在的数据   热点key过期         大量key同时过期
   ↓                ↓                  ↓
缓存无 -> DB无    缓存无 -> DB有      缓存集体失效
   ↓                ↓                  ↓
每次都查DB        瞬间高并发查DB       DB压力暴增

代码级解决方案:

1️⃣ 缓存穿透 --- 布隆过滤器

typescript 复制代码
@Service
public class CacheService {
    
    @Autowired
    private RedissonClient redisson;
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 布隆过滤器:快速判断"一定不存在"或"可能存在"
    public String getData(String key) {
        RBloomFilter<String> bloomFilter = 
            redisson.getBloomFilter("user:bloom");
        
        // 1. 布隆过滤器拦截
        if (!bloomFilter.contains(key)) {
            return null; // 一定不存在,直接返回
        }
        
        // 2. 查缓存
        String value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return value;
        }
        
        // 3. 查数据库(布隆过滤器说有,但实际可能没有)
        return queryDB(key);
    }
}

2️⃣ 缓存击穿 --- 互斥锁 + 逻辑过期

ini 复制代码
public String getHotData(String key) {
    String json = redisTemplate.opsForValue().get(key);
    
    // 1. 判断是否逻辑过期
    RedisData redisData = JSON.parseObject(json, RedisData.class);
    if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
        return redisData.getData(); // 未过期,直接返回
    }
    
    // 2. 已过期,尝试获取锁重建缓存
    String lockKey = "lock:" + key;
    boolean isLock = tryLock(lockKey, 10);
    
    if (isLock) {
        // 双重检查,防止重复重建
        json = redisTemplate.opsForValue().get(key);
        redisData = JSON.parseObject(json, RedisData.class);
        
        if (redisData.getExpireTime().isBefore(LocalDateTime.now())) {
            // 开启独立线程重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    String newData = queryDB(key);
                    saveRedisWithLogicalExpire(key, newData, 30L);
                } finally {
                    unlock(lockKey);
                }
            });
        }
    }
    
    // 3. 返回过期数据(保证可用性)
    return redisData.getData();
}

3️⃣ 缓存雪崩 --- 多级缓存 + 随机过期

scss 复制代码
@Configuration
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        // Caffeine 本地缓存 + Redis 分布式缓存
        CaffeineCacheManager localCache = new CaffeineCacheManager();
        localCache.setCaffeine(
            Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
        );
        
        return new CompositeCacheManager(localCache, redisCacheManager());
    }
}

// 设置随机过期时间,避免同时失效
public void setWithRandomExpire(String key, String value, long baseExpire) {
    // 基础过期时间 + 随机 0-300 秒
    long expire = baseExpire + RandomUtil.randomLong(0, 300);
    redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}

💡 记忆口诀:

  • 穿透查不到(布隆挡)

  • 击穿过期了(锁来保)

  • 雪崩集体挂(多级+随机)


🔥 问题三:分布式锁用 setnx 就够了吗?

面试官潜台词: 考察你对分布式锁完整实现的理解。

90% 的人踩坑:

typescript 复制代码
// 错误示范!生产环境别这么写
public void wrongLock(String key) {
    Boolean success = redisTemplate.opsForValue()
        .setIfAbsent(key, "1"); // setnx
    
    if (success) {
        // 执行业务逻辑...
        // 如果这里抛异常或宕机,锁永远释放不了!
        redisTemplate.delete(key);
    }
}

❌ 3 个致命问题:

  1. 没设置过期时间 → 死锁
  2. 业务没执行完就过期 → 误删别人的锁
  3. 不是原子操作 → 竞态条件

正确姿势:Redisson 实现

csharp 复制代码
@Service
public class OrderService {
    
    @Autowired
    private RedissonClient redisson;
    
    public void createOrder(Long userId) {
        // 1. 获取锁(可重入、自动续期)
        RLock lock = redisson.getLock("order:user:" + userId);
        
        try {
            // 等待10秒,锁30秒自动过期,看门狗自动续期
            boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
            
            if (!isLock) {
                throw new RuntimeException("获取锁失败,请稍后重试");
            }
            
            // 2. 执行业务(锁内操作要尽可能快!)
            doCreateOrder(userId);
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁被中断");
        } finally {
            // 3. 释放锁(判断是不是自己加的锁)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Redisson 看门狗原理:

markdown 复制代码
业务线程
   │
   ├── 获取锁成功 ──┐
   │                │
   │                ▼
   │         启动看门狗(Watch Dog)
   │         每隔 10 秒检查:锁还在吗?
   │                │
   │                ├── 还在 → 续期到 30 秒
   │                └── 不在了 → 停止看门狗
   │
   ├── 业务执行中... ──┐
   │                   │
   └── 业务完成 ───────┘
                       │
                       ▼
                  释放锁 + 停止看门狗

💡 核心要点:

  • 原子性:加锁必须带过期时间(Lua脚本保证)

  • 可重入:同一线程可以多次获取锁

  • 自动续期:看门狗防止业务未完成锁过期

  • 谁加谁删:释放锁前判断持有者


📝 面试总结

问题 核心考点 一句话答案
单线程快? IO模型、内存操作 单线程+IO多路复用,无锁无切换
三大问题? 缓存架构风险 穿透布隆挡、击穿孔锁保、雪崩多级+随机
分布式锁? 锁的完整性 setnx+过期+看门狗+Lua原子性
相关推荐
用户9623779544814 分钟前
代码审计 | CC2 链 —— _tfactory 赋值问题 PriorityQueue 新入口
后端
Vfw3VsDKo3 小时前
Maui 实践:Go 接口以类型之名,给 runtime 传递方法参数
开发语言·后端·golang
是真的小外套4 小时前
第十五章:XXE漏洞攻防与其他漏洞全解析
后端·计算机网络·php
ybwycx6 小时前
SpringBoot下获取resources目录下文件的常用方法
java·spring boot·后端
小陈工6 小时前
Python Web开发入门(十一):RESTful API设计原则与最佳实践——让你的API既优雅又好用
开发语言·前端·人工智能·后端·python·安全·restful
小阳哥AI工具6 小时前
Seedance 2.0使用真人参考图生成视频的方法
后端
IeE1QQ3GT7 小时前
使用ASP.NET Abstractions增强ASP.NET应用程序的可测试性
后端·asp.net
Full Stack Developme7 小时前
SpringBoot多线程池配置
spring boot·后端·firefox
sxhcwgcy9 小时前
SpringBoot 使用 spring.profiles.active 来区分不同环境配置
spring boot·后端·spring
稻草猫.11 小时前
Spring事务操作全解析
java·数据库·后端·spring