本文目录导读:

下面给你一套 Java 面试中关于 Redis 的高频案例讲解,从简单到深入,覆盖缓存穿透、击穿、雪崩、分布式锁、数据一致性等核心场景,并附上代码示例和回答话术。
高频案例:缓存穿透 / 击穿 / 雪崩
这是面试官必问的“三兄弟”,核心考察你对缓存异常场景的理解和解决方案。
缓存穿透(查不存在的数据)
现象:大量请求查询一个缓存和数据库都不存在的 key,导致请求直接打到数据库。
解决方案:
- 缓存空值:如果数据库查不到,也把空值缓存起来,设置较短的过期时间(如 5 分钟)。
- 布隆过滤器:在缓存之前加一层 Bloom Filter,拦截不存在的 key。
代码示例(缓存空值):
public String getData(String key) {
// 1. 查缓存
String value = redis.get(key);
if (value != null) {
return value;
}
// 2. 缓存未命中,查数据库
String dbValue = queryDB(key);
if (dbValue == null) {
// 3. 缓存空值,防止穿透
redis.set(key, "", 300); // 5分钟过期
return null;
}
// 4. 写回缓存
redis.set(key, dbValue, 3600);
return dbValue;
}
缓存击穿(热点 key 过期)
现象:一个热点 key 在过期瞬间,大量并发请求同时打向数据库。
解决方案:
- 互斥锁:只允许一个线程去重建缓存,其他线程等待。
- 逻辑过期:不设置物理过期时间,而是存一个逻辑过期时间,异步更新。
代码示例(互斥锁):
public String getDataWithLock(String key) {
String value = redis.get(key);
if (value != null) {
return value;
}
// 加锁(使用 Redis SETNX 实现)
String lockKey = "lock:" + key;
boolean locked = redis.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (locked) {
try {
// 二次检查(double check)
value = redis.get(key);
if (value != null) return value;
// 查数据库
value = queryDB(key);
redis.set(key, value, 3600);
return value;
} finally {
redis.delete(lockKey);
}
} else {
// 等待重试
Thread.sleep(100);
return getDataWithLock(key); // 递归重试
}
}
缓存雪崩(大量 key 同时过期)
现象:大量 key 在同一时间过期,导致请求全部打到数据库。
解决方案:
- 随机过期时间:在设置过期时间时加上随机值。
- 多级缓存:本地缓存(Caffeine)+ Redis。
- 熔断降级:数据库压力过大时,直接返回默认值。
// 设置过期时间时加随机值 int expireTime = 3600 + new Random().nextInt(600); // 1小时 + 0~10分钟随机 redis.set(key, value, expireTime);
分布式锁(重点考察)
场景
在分布式系统中,多个服务同时操作同一个资源(如库存扣减),需要保证互斥性。
要求
- 互斥性:同一时刻只有一个线程能获取锁。
- 防死锁:锁必须设置过期时间。
- 防误删:只能删除自己的锁(加 value 标识)。
代码示例(Redisson 实现)
@Autowired
private RedissonClient redissonClient;
public void deductStock() {
RLock lock = redissonClient.getLock("stock:1001");
try {
// 尝试加锁,等待 10 秒,锁有效 30 秒
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务逻辑:扣减库存
int stock = getStock();
if (stock > 0) {
reduceStock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
面试关键点:
- 为什么不用 SETNX 自己实现?—— Redisson 有watch dog 自动续期、可重入等特性。
- 锁过期时间设置多少合适?—— 要结合业务执行时间,或使用 Redisson 自动续期。
缓存与数据库双写一致性
场景
更新数据库后,如何保证缓存一致?
方案对比
| 方案 | 说明 | 问题 |
|---|---|---|
| 先更新数据库,再删缓存 | 推荐 | 可能短暂不一致,但最终一致 |
| 先删缓存,再更新数据库 | 不推荐 | 并发下容易脏数据 |
| 延迟双删 | 先删缓存 → 更新数据库 → 延迟再删 | 解决并发问题 |
推荐方案(先更新 DB,再删缓存)
@Transactional
public void updateData(String id, String newValue) {
// 1. 更新数据库
database.update(id, newValue);
// 2. 删除缓存
redis.delete("key:" + id);
}
补充(延迟双删):
public void updateData(String id, String newValue) {
redis.delete("key:" + id); // 先删缓存
database.update(id, newValue);
// 延迟 500ms 再删一次
Thread.sleep(500);
redis.delete("key:" + id);
}
热点数据缓存(商品详情)
场景
商品详情页高并发访问量巨大,如何设计?
方案
- 多级缓存:本地缓存(Caffeine)→ Redis → 数据库。
- 缓存预热:项目启动时,把热门商品加载到缓存。
- 缓存更新策略:定时刷新 + 主动失效。
// 缓存预热
@Component
public class CachePreload implements ApplicationRunner {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(ApplicationArguments args) {
List<Product> hotProducts = productService.getHotProducts();
for (Product product : hotProducts) {
redisTemplate.opsForValue().set("product:" + product.getId(), product, 1, TimeUnit.HOURS);
}
}
}
排行榜(ZSet 应用)
场景
游戏排行榜、销售榜单等。
代码示例
// 更新分数
redisTemplate.opsForZSet().add("rank", "user:1001", 85.5);
// 获取 Top 10
Set<Object> top10 = redisTemplate.opsForZSet().reverseRange("rank", 0, 9);
// 获取某用户排名
Long rank = redisTemplate.opsForZSet().reverseRank("rank", "user:1001");
面试回答话术模板
面试官问:“你们项目中 Redis 是怎么用的?”
建议从以下三方面回答:
缓存场景:商品详情缓存、用户信息缓存,使用 Spring Cache + Redis。 2. 分布式锁:库存扣减时使用 Redisson 分布式锁防止超卖。 3. 排行榜/计数器:点赞用 incr,排行榜用 ZSet。
然后引出经典问题:
“在缓存使用过程中,我们也考虑到了缓存穿透、击穿、雪崩。 穿透我们用缓存空值+布隆过滤器; 击穿我们用互斥锁; 雪崩用过期时间加随机值。 对于一致性,我们采用先更新数据库再删缓存的策略,最终一致即可。”
备战建议
| 知识点 | 必备程度 | 面试频率 |
|---|---|---|
| 缓存穿透/击穿/雪崩 | 90% | |
| 分布式锁 | 80% | |
| 缓存一致性 | 70% | |
| ZSet 排行榜 | 40% | |
| 持久化机制(RDB/AOF) | 50% | |
| 过期删除策略 | 30% |