缓存雪崩
现象
缓存雪崩是指缓存数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库(redis主机挂了,Redis全盘崩溃、比如缓存中有大量数据同时过期)
解决方案
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
- 设置热点数据永远不过期
- 使用缓存预热
缓存预热就是将数据提前加入到缓存中,当数据发生变更,再将最新的数据更新到缓存
缓存穿透
现象
请求去查询一条记录,先redis后mysql发现都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设(redis中没有、mysql也没有)
解决方案一:空对象缓存或者缺省值
黑客会对你的系统进行攻击,拿一个不存在的id去查询数据,会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉
- id相同打你系统:第一次打到mysql,空对象缓存后第二次就返回null了,避免mysql被攻击,不用再到数据库中去走一圈了
- id不同打你系统:由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)
解决方案二:Google布隆过滤器Guava解决缓存穿透-单机版
/**
* 测试布隆过滤器(可用于redis缓存穿透)
*
*/
public class TestBloomFilter {
private static int total = 1000000;
private static BloomFilter bf = BloomFilter.create(Funnels.integerFunnel(), total);
public static void main(String[] args) {
// 初始化ID数据到过滤器中
for (int i = 0; i < total; i++) {
bf.put(i);
}
for (int i = 0; i < total; i++) {
if (!bf.mightContain(i)) {
// todo
}
}
}
}
解决方案三:Redis布隆过滤器解决缓存穿透
Guava 提供的布隆过滤器的实现还是很不错的 (想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用 ,而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到Redis中的布隆过滤器了
注:Redis在4.0 之后有了插件功能(Module),可以使用外部的扩展功能,可以使用RedisBloom作为Redis布隆过滤器插件。
实现案例
缓存击穿
现象
缓存击穿:大量的请求同时查询一个key时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去(简单说就是热点key突然失效了,暴打mysql)
危害:会造成某一时刻数据库请求量过大,压力剧增
方案一:互斥更新、随机退避、差异失效时间
方案2:对于访问频繁的热点key,干脆就不设置过期时间
方案3:互斥独占锁防止击穿
public User findUserById2(Integer id) {
User user = null;
String key = CACHE_KEY_USER+id;
//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
user = (User) redisTemplate.opsForValue().get(key);
if(user == null) {
//2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
synchronized (UserService.class){
user = (User) redisTemplate.opsForValue().get(key);
//3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
if (user == null) {
//4 查询mysql拿数据
user = userMapper.selectByPrimaryKey(id);//mysql有数据默认
if (user == null) {
return null;
}else{
//5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
//setnx 不存在才创建
redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
}
}
}
}
return user;
}