title: 缓存三大问题解决方案
date: 2021/05/25 10:57
缓存穿透
缓存穿透指的是查询一个一定不存在的数据,由于存储层在查不到数据时不写入缓存,这将导致这个不存在的数据每次请求都要到存储层查询,从而失去了缓存的意义。
如果有人利用不存在的key频繁攻击我们的应用,可能 DB 就挂掉了。
解决方案
- 布隆过滤器,将所有可能存在的数据 hash 到一个足够大的 bitmap 中,一个一定不存在的数据则会被拦截掉,从而避免了对底层存储系统的查询压力。
因为 BloomFilter 如果声称其中包含元素,则可能是错误的,但是如果声明其中不包含元素,则肯定是正确的。
- 空值缓存:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟,这样则可以应对短时间的大量的该key攻击,设置为较短的失效时间是因为该值可能业务无关,存在意义不大,且该次的查询也未必是攻击者发起,无过久存储的必要,故可以早点失效。
缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
另一种情况是缓存服务器宕机,也会造成雪崩现象。
解决方案
-
永不过期:简单粗暴
-
线程互斥:通过分布式锁只允许一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据才可以,每个时刻只有一个线程在执行请求,减轻了db的压力,但缺点也很明显,降低了系统的qps。
-
交错失效时间:这种方法时间比较简单粗暴,既然在同一时间失效会造成请求过多雪崩,那我们错开不同的失效时间即可从一定长度上避免这种问题,在缓存进行失效时间设置的时候,从某个适当的值域中随机一个时间作为失效时间即可。
-
请求限流:
- 限制请求数量,常见的限流算法有滑动窗口,令牌桶算法和漏桶算法,或者直接使用队列、加锁(信号量)等
- 限制数据库的每秒请求数,避免数据库挂掉。对于被限流的请求,采用服务降级处理,比如提供默认的值,或者空白值
-
本地缓存:如果使用本地缓存,即使分布式缓存挂了,也可以将数据库查询的结果缓存到本地,避免后续请求全部达到数据库中。
-
缓存高可用:使用Redis Sentinel等搭建缓存的高可用,避免缓存挂掉无法提供服务的情况,从而降低出现缓存雪崩的情况
缓存击穿
缓存击穿实际上是缓存雪崩的一个特例,对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,由于系统中对这些热点的数据缓存也存在失效时间,在热点的缓存到达失效时间时,此时可能依然会有大量的请求到达系统,没有了缓存层的保护,这些请求同样的会到达db从而可能引起故障。击穿与雪崩的区别即在于击穿是对于特定的热点数据来说,而雪崩是全部数据。
解决方案
- 永不过期:简单粗暴
- 请求限流:
- 限制请求数量,常见的限流算法有滑动窗口,令牌桶算法和漏桶算法,或者直接使用队列、加锁(信号量)等
- 限制数据库的每秒请求数,避免数据库挂掉。对于被限流的请求,采用服务降级处理,比如提供默认的值,或者空白值
- 本地缓存:使用 LRU 做二级缓存
扩展 - 服务雪崩
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩效应” 。
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
5.2 常见的容错方案
容错说白了就是保护自己不被猪队友拖垮的一些措施,下面介绍常见的服务容错思路和组件。
常见的容错思路
1、隔离
它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离。
Hystrix线程隔离技术解析-线程池
Hystrix线程隔离技术解析-信号量
2、超时
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
3、限流
限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
4、熔断
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整 体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断一般有三种状态:
- 熔断关闭状态(Closed) :服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
- 熔断开启状态(Open) :后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法
- 半熔断状态(Half-Open):尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
5、降级
降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案。
常见的容错组件
Hystrix:
Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
Resilience4J:
Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和 prometheus等多款主流产品进行整合。
Sentinel:
Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
Sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数等 | 异常比率模式、超时熔断 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种配置源 | 支持多种数据源 | 有限支持 |
扩展性 | 丰富的 SPI 扩展接口 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
集群流量控制 | 支持 | 不支持 | 不支持 |
流量整形 | 支持预热模式、匀速排队模式等多种复杂场景 | 不支持 | 简单的 Rate Limiter 模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
多语言支持 | Java / C++ | Java | Java |
开源社区状态 | 活跃 | 停止维护 | 较活跃 |
参考文章
缓存三大问题及解决方案
缓存穿透,缓存击穿,缓存雪崩解决方案分析
*缓存失效的场景