缓存雪崩、缓存穿透解决方案以及实战

Redis/缓存系统
374
0
0
2023-01-18

缓存雪崩和缓存穿透并不是一个概念

缓存雪崩

当缓存失效或缓存数据还没有准备就绪时,高并发请求接入时无法阻挡,从而接入数据库导致数据库宕机或者延迟,而数据库又被大量其他服务所依赖导致大面积服务崩溃,最终导致整个系统或网站的崩溃。

解决方案:

1、分布式锁:只有一个线程能获得锁,获得后判断缓存数据是否存在,不存在获取缓存数据并更新缓存,存在获得数据,其他线程阻塞,更新缓存完成后或获得数据后释放锁

2、数据预热:提前将数据缓存好,不等用户请求再缓存数据,如果数据量大,启动后运维手动触发,数据量不大,启动自动加载

3、缓存双层降级策略:当缓存C1失效时,请求缓存备份C2,C1失效时间短,C2失效时间长

4、错峰更新缓存:更新缓存和用户请求高峰期错峰,定制更新策略

5、设置不同的过期时间,让缓存失效时间尽量均匀,这样不会大量用户同时缓存失效

缓存雪崩只对非常高并发的场景才会发生,一般流量的场景不会很严重,然后缓存穿透对一般流量的场景的影响也非常严重

缓存穿透

没有命中一个数据(没查到),导致再查还是没查到直接将大量请求放到数据库上

解决方案:

1、缓存空数据,并设置合适的过期时间以更新缓存

2、布隆过滤器排除不可能有缓存值的请求

布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

img

一条很长的信息通过多个hash函数将其转化为一个bit向量,根据向量的结果判断这条信息是否存在。

优点:

  1. 省空间:位很省空间,一个很长的东西就变成了几位
  2. 省时间:判断、操作位很快,hash算法转化成位向量也很快

缺点:牺牲了一定的准确度

以下是guava实现的布隆过滤器的使用方法:

private BloomFilter<String> bf;
    //创建布隆过滤器(默认3%误差)
    bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), allUsers.size());

    bf.put(userDto.getUserName());
    if(!bf.mightContain(randomUser)){
    System.out.println("bloom filter don't has this user");
    return;
}

综合解决方案

说明:部分思路参考京东到家最佳实践

img

这个图中主要是分布式锁和缓存空值的方式来方式雪崩、穿透

这里主要说明一下分布式锁的细节:获得锁的线程才有机会更新缓存,而没有获得锁的线程在没有出现“死锁问题”的情况下只能等待第一个获得锁的线程将缓存数据准备好,直接享受缓存数据了那么有两种情况对锁的获取是不同的:

情况1:并发请求(多个请求)对应DB的查询对象是同一个

这种情况,并发请求可以竞争同一把锁,比如一个省的用户查询的配置信息都是一致的,那么这个key就可以以省为单位比如110config,一省用户的并发请求都去抢这个110config的锁

情况2:并发请求(多个请求)对应DB查询对象是多个(极端情况1对1)

这种情况,不可以所有并发请求都争抢一把锁,这样逻辑不正确,比如每个用户都有自己的订单数据,每个用户查询数据库的结果也都是不一样的,那么这个key就不能以省为单位,否则缓存的结果就肯定不正确

同样也不可以每个用户(并发请求时,每个用户一次请求,而不是每个用户多次请求)各自都有一把锁,假如1W人同时请求,有1W个请求,如果再设置1W把锁,缓存同时失效的话还是会同时有1W次请求放入DB,DB无法承受,那么这种情况可以让这1W人,争抢128个锁,抢到锁的请求,才有可能去更新缓存

//锁的数量 锁的数量越少 每个用户对锁的竞争就越激烈,直接打到数据库的流量就越少,对数据库的保护就越好,如果太小,又会影响系统吞吐量,可根据实际情况调整锁的个数
public static final String[] LOCKS = new String[128];
//在静态块中将128个锁先初始化出来
static {
    for (int i = 0; i < 128; i++) {
        LOCKS[i] = "lock_" + i;
    }
}

try{
  
  if(exists(cacheKey)){
    return cacheData;
  }
  String[] locks = OrderRedisKey.LOCKS;
//hash分组//缓存中没有,就先上锁,锁的粒度是根据用户Id的hashcode和127取模,这里是个hash的算法,相当于把userID均匀的分布于0-127之间
//a % (2^n) 等价于 a & (2^n - 1)   与运算的前提是2的n次方-1才与%等价,与运算比算术运算更快
int index = userId.hashCode() & (locks.length - 1);
lock(locks[index]);//竞争锁
if(exists(cacheKey)){
  return cacheData;
}
data=getFromDB();
if(data == null){
  //缓存空数据,并设定缓存过期时间
  addcahce(cacheKey,null,ttl_null);
}else{
  //缓存数据,并设定缓存过期时间
  addcahce(cacheKey,data,ttl_notnull);
}
return data;


}finally{
  unlock(locks[index])
}