如何解决Redis雪崩、穿透、并发等5大难题
缓存雪崩
数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。
比如一个雪崩的简单过程:
1、redis集群大面积故障
2、缓存失效,但依然大量请求访问缓存服务redis
3、redis大量失效后,大量请求转向到mysql数据库
4、mysql的调用量暴增,很快就扛不住了,甚至直接宕机
5、由于大量的应用服务依赖mysql和redis的服务,这个时候很快会演变成各服务器集群的雪崩,最后网站彻底崩溃。

如何预防缓存雪崩:

1.缓存的高可用性
缓存层设计成高可用,防止缓存大面积故障。即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。
2.缓存降级
可以利用ehcache等本地缓存(暂时支持),但主要还是对源服务访问进行限流、资源隔离(熔断)、降级等。
当访问量剧增、服务出现问题仍然需要保证服务还是可用的。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级,这里会涉及到运维的配合。
降级的最终目的是保证核心服务可用,即使是有损的。
比如推荐服务中,很多都是个性化的需求,假如个性化需求不能提供服务了,可以降级补充热点数据,不至于造成前端页面是个大空白。
在进行降级之前要对系统进行梳理,比如:哪些业务是核心(必须保证),哪些业务可以容许暂时不提供服务(利用静态页面替换)等,以及配合服务器核心指标,来后设置整体预案,比如:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
3.Redis备份和快速预热
1)Redis数据备份和恢复
2)快速缓存预热
4.提前演练
最后,建议还是在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,对高可用提前预演,提前发现问题。
5.差异化缓存失效时间
在某一时间缓存数据集中失效,导致大量请求穿透到数据库,将数据库压垮。可以在初始化数据时,差异化各个key的缓存失效时间,失效时间 = 一个较大的固定值 + 较小的随机值
缓存穿透
缓存穿透是指查询一个数据库
不存在的数据。例如:从缓存redis没有命中,需要从mysql数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
正常接口查询肯定查询的是数据库存在的数据,如果数据库不存在,只能说明俩种可能,第一种自身业务出现问题,第二种恶意攻击。
解决方法有俩个:
- 返回空对象
如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。设置一个过期时间或者当有值的时候将缓存中的值替换掉即可。
- 布隆过滤器拦截
查看相关布隆过滤器相关只是可查看我的文章。
在访问缓存层和存储层之前,将所有存在的key用布隆过滤器先存储起来,做第一层拦截
比如有个用户下单接口,该接口传递参数userid,然后使用userid为key查询缓存进行下单操作。
那么该接口就可以使用布隆过滤器来过滤userid,我们首先将系统的所有userid都加入到布隆过滤器中,当请求的userid使用布隆过滤器过滤之后发现不存在,那么直接返回客户端即可,并不需要查询缓存层和数据层了;如果布隆过滤器过滤之后发现useid可能存在(布隆控制器存在误判情况,只能判断可能存在,而不能断定一定存在)
的话就可以继续执行流程:先读取缓存,如果缓存存在直接返回客户端,如果不存在,则查询后端存储,如果后端存储查询到了数据就写入缓存,最后返回客户端,由于布隆过滤器存在误判情况
,所以如果后端存储查询不到了数据,则结合第一种方式返回给客户端空对象,并写入缓存
。
总结:
由于布隆过滤器存在误判情况,所以使用过滤器方式和返回空对象方式必须结合使用。
排除自身业务的问题,假设遇到恶意攻击,传递的userid都不是真实的,也就是数据库中不存在的。针对这俩种结合的方案分析如下,如果第一层布隆过滤器判断不存在,
则直接在缓存层之前就给过滤了,就不会到达缓存层和数据层了,可以大大减少压力。
如果布隆过滤器认为可能存在后,那么就会到达缓存层和数据层了,由此可见使用布隆过滤器方案的必要性。
缓存并发
其实redis自身就是单线程操作,redis本身并没有锁的概念,按照先到先执行的原则,先到的先执行,其余的阻塞。但是利用predis phpredis等客户端对Redis进行并发访问时会出现问题。典型的例子就是库存超卖,解决方案有以下俩种
- 这里可以使用redis的分布式锁可以解决并发问题。如命令set k v px ms nx,该命令在k不存在时才赋值k。也就是说如果返回true,则代表获取锁成功,如果返回false则代表已有资源获取锁,此时需要轮训,处于阻塞状态。
- 可以将redis操作放在队列中使其串行化,必须的一个一个执行,如果放到队列进行串行化的话,效率会急剧下降。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。
这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
目的就是在系统上线前,将数据加载到缓存中。
以上就是缓存雪崩、预热、降级等的介绍。
缓存热点
-
加节点
有些热点数据访问量会特别大,单个缓存节点(例如Redis)无法支撑这么大的访问量。如果是读请求访问量大,可以考虑读写分离,一主多从的方案,用从节点分摊读流量;如果是写请求访问量大,可以采用集群分片方案,用分片分摊写流量。以秒杀扣减库存为例,假如秒杀库存是100,可以分成5片,每片存20个库存。 -
拆分复杂结构
比如当前key是一个hash结构,可以将该key分解为多个string类型,让其分布在不同节点,进行降低压力 -
迁移热点key
以redis cluster为例,可以新增一台全新的节点,将热点key的slot迁移到该节点上,该节点的cpu,内存,io都需要好点。 -
本地缓存加发布订阅机制
要解决单点存在hot key的问题,就通过多机分流来解决,而本地缓存就是一种分流方案。由本地缓存来分担流量,这样即使有热点key存在,有多少业务系统进程就有多少相互独立的缓存来分担流量,可以很好的解决热点key的问题。
本地缓存解决了热点,但同时也带来了数据一致性的问题,在短时间内同一客户端访问业务系统可能会取到不一致的缓存结果,通常可使用redis的发布订阅功能来通知所有本地缓存。
-
降级服务
如果节点读压力过大,我们可以关闭一些不重要的读方面的服务,以保证核心业务的正常运行,必须可以关闭商品查看评论功能或者关闭查看物流信息功能。
如果节点写压力过大,我们可以关闭一些次要功能,累死用户商品评价功能 -
熔断机制
如果redis服务阻塞,将会阻塞业务线程,为了避免消耗完线程池内的所有的线程,我们需要有熔断机制, 熔断机制的本质就是fail fast,如果在一定内时间没有返回数据,我们可以触发熔断机制,触发回调函数,回调函数内用户可以自己定义,上面介绍的使用本地缓存方案就是一种熔断机制。
热点key重建优化
开发人员使用“缓存+过期时间”的策略即可以加速数据读写,又保证了数据的定时更新,这种模式基本可以满足绝大部分需求,但是如果3个条件同时出现,可能会对系统造成致命的危害
- 当前key是热点key,并发量非常大
- 当前key缓存正好失效
- 重建缓存不能在短期时间内完成,可能是一个复杂计算
正常业务流程为,先读取缓存,如果缓存存在直接返回客户端,如果不存在,则查询后端存储并进行计算,然后写入缓存,最后返回客户端,伪代码如下
//先读取缓存
value=redis.get(key)
if(value == null){
// 读取数据库等系列操作,并计算,获得变量v
value=计算获得value
redis.set(key,value)
}
....
处理业务
.....
return value
比如此代码并发很高,就出现如下问题:
如果第一个线程还有执行完redis的set操作时,第二个线程在执行到get操作时,返现value为null,所以第二个线程也会执行redis的set操作,如果并发很高,可能第三个,第四个。。。。线程都会执行redis的set操作,而set操作通常涉及到存储层查询,这样就给mysql大大增加了压力,最坏的情况可能直接导致mysql宕机,总结一句话就是有大量线程来重建缓存,造成后端负载加大,甚至让应用直接奔溃.
我们可以使用redis的分布式锁来解决这一个问题,伪代码如下
//先读取缓存
value=redis.get(key)
if(value == null){
mutexKey=time()
if(redis.set("mutex".key,mutexKey,'px',1000,'nx')){
// 读取数据库等系列操作,并计算,获得变量v
value=计算获得value
redis.set(key,value)
}else{
//进入这个分支的就是并发线程
sleep(0.1)//阻塞0.1s,时间需要根据实际情况而定,必须保证大于value复杂计算的时间
value=redis.get(key)
}
}
....
处理业务
.....
return value
上面这段代码就保证了只有一个线程会到达后端的存储层,成功缓解了存储层的压力。n
网友评论