🔥 问题一: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 个致命问题:
- 没设置过期时间 → 死锁
- 业务没执行完就过期 → 误删别人的锁
- 不是原子操作 → 竞态条件
正确姿势: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原子性 |