Redis系列(五)缓存雪崩、缓存穿透、缓存击穿及管道

缓存雪崩

我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期,所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

缓存正常从Redis中获取,示意图如下:
Redis系列(五)缓存雪崩、缓存穿透、缓存击穿及管道_第1张图片
缓存失效瞬间示意图如下:
Redis系列(五)缓存雪崩、缓存穿透、缓存击穿及管道_第2张图片
解决方案:

(1)碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪代码如下:

//伪代码
public object GetProductListNew() {
     
    int cacheTime = 30;
    String cacheKey = "product_list";
    String lockKey = cacheKey;
 
    String cacheValue = CacheHelper.get(cacheKey);
    if (cacheValue != null) {
     
        return cacheValue;
    } else {
     
        synchronized(lockKey) {
     
            cacheValue = CacheHelper.get(cacheKey);
            if (cacheValue != null) {
     
                return cacheValue;
            } else {
     
                //这里一般是sql查询数据
                cacheValue = GetProductListFromDB(); 
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            }
        }
        return cacheValue;
    }
}

加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

(2)还有一个解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下:

//伪代码
public object GetProductListNew() {
     
    int cacheTime = 30;
    String cacheKey = "product_list";
    //缓存标记
    String cacheSign = cacheKey + "_sign";
 
    String sign = CacheHelper.Get(cacheSign);
    //获取缓存值
    String cacheValue = CacheHelper.Get(cacheKey);
    if (sign != null) {
     
        return cacheValue; //未过期,直接返回
    } else {
     
        CacheHelper.Add(cacheSign, "1", cacheTime);
        ThreadPool.QueueUserWorkItem((arg) -> {
     
            //这里一般是 sql查询数据
            cacheValue = GetProductListFromDB(); 
            //日期设缓存时间的2倍,用于脏读
            CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                 
        });
        return cacheValue;
    }
} 

解释说明:

1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一各被称为“二级缓存”的解决方法。

如何预防缓存雪崩
1).保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster
2). 依赖隔离组件为后端服务限流并降级。比如使用Hystrix限流降级组件
3). 提前演练,在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,再次基础上做一些预案设定。

缓存穿透

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层获取,失去了缓存保护后端的意义。
造成缓存穿透的基本原因有两个:
第一,自身业务代码或者数据出现问题。
第二,一些恶意攻击,爬虫等造成大量空命中

解决方案

1.布隆过滤器

对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在,这个值可能不存在;当他说不存在时,那就肯定不存在。

这种方法适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用很少。

2.一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!

这种做法如果针对高并发或大数据量请求下,会造成上万个空的value值存在redis中,大量浪费redis内存空间。

缓存击穿

热点key失效后(和缓存雪崩的区别,缓存雪崩是大量key同时失效,造成缓存雪崩,而缓存击穿,是热点key可能一个key失效,就会造成大量阻塞),大量请求同时请求到了后台。

解决方案

开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:
当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大
重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。
在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。
要解决这个问题主要就是要避免大量线程同时重建缓存。
我们可以利用分布式互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。

伪代码如下:

String get(String key){
     
	// 从Redis中获取数据
	String value = redis.get(key);
	// 如果value为空,则开始重构缓存
	if(value ==null){
     
		String mutexKey ="mutext:key:"+ key;
		if(redis.set(mutexKey,"1","ex 180","nx")){
      //给这个key上一把锁,ex表示只有一个线程能执行,过期时间为180秒
			// 从数据源获取数据
			value = db.get(key);
			// 回写Redis,并设置过期时间
			redis.setex(key, timeout, value);
			// 删除key_mutex
			redis.delete(mutexKey);
		}// 其他线程休息50毫秒后重试
		else{
     
			Thread.sleep(50);
			get(key);
		}
	}
	return value;
}
Redis管道(pipeline)

pipeline是非原子操作
pipeline可以打包不同的命令,原生做不到
pipeline需要客户端和服务器端同时支持

Redis客户端与Redis服务器之间使用TCP协议进行连接,一个客户端可以通过一个socket连接发起多个请求命令。每个请求命令发出后client通常会阻塞并等待redis服务器处理,redis处理完请求命令后会将结果通过响应报文返回给client,因此当执行多条命令的时候都需要等待上一条命令执行完毕才能执行。比如:
Redis系列(五)缓存雪崩、缓存穿透、缓存击穿及管道_第3张图片
 由于通信会有网络延迟,假如client和server之间的包传输时间需要0.125秒。那么上面的三个命令6个报文至少需要0.75秒才能完成。这样即使redis每秒能处理100个命令,而我们的client也只能一秒钟发出四个命令。这显然没有充分利用 redis的处理能力。

而管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline通过减少客户端与redis的通信次数来实现降低往返延时时间,而且Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。 Pipeline 的默认的同步的个数为53个,也就是说arges中累加到53条数据时会把数据提交。其过程如下图所示:client可以将三个命令放到一个tcp报文一起发送,server则可以将三条命令的处理结果放到一个tcp报文返回。
Redis系列(五)缓存雪崩、缓存穿透、缓存击穿及管道_第4张图片

需要注意到是用 pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。管道中其那面的命令失败,后面的命令不会有影响,继续执行。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试

你可能感兴趣的:(Redis系列,分布式,redis,数据库,nosql)