时间:2026-03-24 14:45
人气:
作者:admin
Redis缓存是Java开发中最常用的技术之一,但缓存穿透、击穿、雪崩三大问题也是面试高频考题。本文结合实战代码,带你彻底搞懂这三大难题。
查询一个数据库和缓存中都不存在的key,每次请求都打到数据库,大量请求可能拖垃数据库。
1. 缓存空对象
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
String key = "user:" + id;
// 查询缓存
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
return user.getId() == null ? null : user; // 返回空对象表示不存在
}
// 查询数据库
user = userMapper.selectById(id);
if (user == null) {
// 缓存空对象,设置短过期时间
redisTemplate.opsForValue().set(key, new User(), 5, TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
return user;
}
}
2. 布隆过滤器:将所有合法key存入Bloom Filter,请求先过滤器,不存在的key直接拖到。
查询一个缓存刚好失效的key,大量请求同时打到数据库。
互斥锁(推荐)
public User getUserById(Long id) {
String key = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) return user;
// 加分布式锁
String lockKey = "lock:user:" + id;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 再次查询缓存(可能其他线程已经写入)
user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
user = userMapper.selectById(id);
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 未获得锁,稍等重试
Thread.sleep(50);
return getUserById(id);
}
return user;
}
大量缓存key同时失效,大量请求同时打到数据库。
1. 过期时间加随机偶数
// 设置缓存时加随机偶数,避免同时失效
int randomExpire = 30 + new Random().nextInt(10); // 30~40分钟
redisTemplate.opsForValue().set(key, user, randomExpire, TimeUnit.MINUTES);
2. 多级缓存:本地缓存(Caffeine)+ Redis两级,即使 Redis雪崩也有本地缓存托底。
3. 缓存预热:项目启动时提前将热点数据加载到缓存。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缓存穿透 | key不存在 | 空对象 / 布隆过滤器 |
| 缓存击穿 | key失效瞬间 | 互斥锁 / 逆机制 |
| 缓存雪崩 | 大量同时失效 | 随机过期 / 多级缓存 |
本文由AI辅助创作。
上一篇:揭秘MySQL索引分类