【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)

文章目录

文章目录

一、前言

二、缓存

2.1 缓存本质 + 缓存分类 + 缓存三大特征 + 三种淘汰算法 + 根据业务场景设计过期时间

2.2 本地缓存(进程内缓存)

2.3 分布式缓存

2.4 缓存五问题:缓存雪崩 + 缓存穿透 + 缓存击穿 + 缓存更新 + 数据不一致

2.4.1 缓存雪崩问题

2.4.2 缓存穿透问题

2.4.3 缓存击穿问题

2.4.4 缓存更新问题(四种缓存更新方式 + 缓存数据源:DB和远程服务)

2.4.5 缓存数据不一致问题

三、限流(服务限流,表示处理的对象是服务,包括限流四规则 + 限流四实现)

3.1 限流

3.2 限流四规则

3.3 限流四实现

四、降级(即服务降级,表示处理的对象是服务,这里指主动降级)

4.1 服务降级的定义

4.2 服务降级五种方式

五、熔断(即服务熔断,表示处理的对象是服务,就是被动降级)

5.1 服务雪崩

5.2 服务雪崩产生的原因

5.3 熔断机制是应对雪崩效应的一种微服务链路保护机制

六、面试金手指

6.1 限流四规则 + 限流四实现

6.2 五种措施小结

七、小结


一、前言

二、缓存

2.1 缓存本质 + 缓存分类 + 缓存三大特征 + 三种淘汰算法 + 根据业务场景设计过期时间

缓存本质:一个数据模型对象。
缓存作用(一句化小结):解决 软件响应速度要求快 和 数据库中查找速度慢 的问题。
缓存分为本地缓存和分布式缓存两种(分级缓存就是应用缓存+分布式缓存):
分布式缓存(即进程间缓存)如redis、memcached等,
本地缓存(即进程内缓存)如ehcache、GuavaCache、Caffeine等。

缓存三大特征
第一特征,命中率
定义:命中率=命中数/(命中数+没有命中数)当某个请求能够通过访问缓存而得到响应时,称为缓存命中。缓存命中率越高,缓存的利用率也就越高。
第二特征,最大空间
定义:最大空间表示缓存中可以容纳最大元素的数量。当缓存存放的数据超过最大空间时,就需要根据淘汰算法来淘汰部分数据存放新到达的数据。
第三特征,淘汰算法
定义:缓存的存储空间有限制,当缓存空间被用满时,要求保证在稳定服务的同时有效提升命中率,这就由缓存淘汰算法来处理,设计适合自身数据特征的淘汰算法能够有效提升缓存命中率。

三种淘汰算法:业务驱动技术,如何选择淘汰算法
FIFO(first in first out) 先进先出
定义:最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据(定义2:不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,但是,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除)。
比较的对象:策略算法主要比较缓存元素的创建时间。
业务场景:适用于保证高频数据有效性场景,优先保障最新数据可用。
LFU(less frequently used)「最少使用」
定义无论是否过期根据元素的被使用次数判断,清除使用次数较少的元素释放空间。
比较的对象:策略算法主要比较元素的hitCount(命中次数)。
业务场景:适用于保证高频数据有效性场景。
LRU(least recently used)「最近最少使用」
定义无论是否过期根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。
比较的对象:策略算法主要比较元素最近一次被get使用时间。
业务场景:比较适用于热点数据场景,优先保证热点数据的有效性。

面试题:考察淘汰算法,手写一个LRU算法?
回答:继承LinkedHashMap,因为它里面有一个LRU算法,
回答1: final Map timeOutInfoHolderMap =
Collections.synchronizedMap(new LinkedHashMap(100, (float) 0.75, true){
// removeEldestEntry是LinkedHashMap中的一个方法
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 100; // 调用LRU淘汰算法的阈值是100
}
});
class TimeOutInfoHolder{}
解释1:继承LinkedHashMap得到淘汰算法,内部就是就是一个双向链表,true表示按访问顺序排序
解释2:该淘汰算法的阈值是100,当前容量size()大于100,返回为true,开始执行LRU策略淘汰对象:将最近最少未使用的 TimeoutInfoHolder 对象剔除掉。
回答2:
class LRU extends LinkedHashMap implements Map {
private final int CACHE_SIZE; // 定义一个final类型变量,final表示在构造函数中赋值后就不会再改变,该变量表示是否调用这个自定义的LRU淘汰算法的阈值
public LRU(int cacheSize) {
// super调用LinkedHashMap的构造函数,第一个参数为初始容量,第二个参数为装载因子,
// 第一个参数按照访问顺序排序,true表示按照访问顺序排序,false表示按照插入顺序排序
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Entry eldest) {
// 当前容量大于阈值,返回为true,作为使用淘汰算法淘汰的重要条件,
// 当前容量小于等于阈值,可以暂时不用考虑淘汰
return size() > CACHE_SIZE;
}
}
解释1:继承LinkedHashMap得到淘汰算法,内部就是就是一个双向链表,true表示按访问顺序排序
解释2:该淘汰算法的阈值是100,当前容量size()大于100,开始淘汰算法

关于缓存:业务驱动技术,如何设置过期时间,以一个飞机票订单为例
比如实际工作中我们对于订单详情的一个缓存。我们可能会根据订单的状态来来构建缓存。
第一,对于已出行、或者已经取消的订单我们基本上是不会去管的(因为订单状态已经终止了,不会再有对于订单状态的写请求),
1.1 对于订单读操作:实时性要求没那么高,从缓存中拿
所以,对于这种订单我们设置的过期时间是不是就可以久一点,比如7天或者30天
第二,对于未出行即将起飞的订单,这时候顾客是不是就会频繁的去刷新订单看看,看看有没有晚点什么的,或者登机口是在哪。
2.1 对于读操作:实时性要求没那么高,从缓存中拿
2.2 对于订单写操作,需要更改订单的状态的(比如退票、改签),可以直接不走缓存,直接查询并修改数据库。
一般来说,读多写少。
所以,对于这种实时性要求比较高的订单我们过期时间还是要设置的比较短的

2.2 本地缓存(进程内缓存)

本地缓存定义:应用和缓存都在同一个进程里面,
本地缓存优点:获取缓存数据的时候纯内存操作,没有额外的网络开销,速度非常快。
本地缓存缺点
本地缓存与业务系统耦合在一起,应用之间无法直接共享缓存的内容。需要每个应用节点单独的维护自己的缓存。每个节点都需要一份一样的缓存,对服务器内存造成一种浪费。本地缓存机器重启、或者宕机都会丢失。
本地缓存存放不可变数据:它适用于缓存一些应用中基本不会变化的数据,比如(国家、省份、城市等),一般可以在应用启动的时候,把需要的数据加载到系统中。

本地缓存三种更新缓存的方式
更新缓存1-定时变量更新(实时性不高):在应用中起一个定时任务(「ScheduledExecutorService」、「TimerTask」等),让它每隔多久去加载变更(数据变更之后可以修改数据库最后修改的时间,每次查询变更数据的时候都可以根据这个最后变更时间加上半小时大于当前时间的数据)的数据重新到缓存里面来。
更新缓存2-定时全量更新(实时性不高):如果觉得这个定时变量更新比较最后修改时间戳麻烦,可以使用全量更新(就跟项目启动加载数据一样)。这种方式的话,对数据更新可能会有点延迟。可能这台机器看到的是更新后的数据,那台机器看到的数据还是老的(机器发布时间可能不一样)。
无论是定时变量更新还是定时变量更新,实时性都不高,所以有了广播订阅mq消息更新。
更新缓存3-广播订阅mq消息(实时性高):如果对实时性有要求的话,使用广播订阅mq消息更新。一旦有数据更新mq会把更新数据推送到每一台机器,实时性好,但是实现起来较为复杂。
小结:本地缓存实时性排名:广播订阅mq消息更新>定时变量更新>定时全量更新。

本地缓存举例
【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第1张图片
常见本地缓存有以下几种实现方式:从上述表格我们看出性能最佳的是Caffeine。实际开发中,关于这个本地缓存作为整个多级缓存系统的一部分,里面提供了丰富的api,以及各种各样的淘汰算法。

2.3 分布式缓存

分布式缓存
分布式缓存定义:与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
分布式缓存举例:常见的分布式缓存有redis、MemCache等。
分布式缓存的应用
在高并发的环境下,比如春节抢票大战,一到放票的时间节点,分分钟大量用户以及黄牛的各种抢票软件流量进入12306,这时候如果每个用户的访问都去数据库实时查询票的库存,大量读的请求涌入到数据库,瞬间Db就会被打爆,cpu直接上升100%,服务马上就要宕机或者假死。即使进行了分库分表也是无法避免的。为了减轻db的压力以及提高系统的响应速度。一般都会在数据库前面加上一层缓存,甚至可能还会有多级缓存。

  本地缓存 分布式缓存
定义 对进程的内存中进行缓存,如JVM堆中,如LRUMap、Ehcache  
优点 完全基于内存访问,没有远程交互开销,性能最好 不受限于单机容量,具有良好的水平扩展能力,对较大数据量的场景也能应付自如
缺点 受限于单机容量,一般缓存较小且无法扩展 需要进行远程请求,性能不如本地缓存

金手指:多级缓存(本地缓存+分布式缓存)
为了平衡本地缓存和分布式缓存,实际业务中一般采用多级缓存,
本地缓存中只保存访问频率最高的部分热点数据,
分布式缓冲中保存其他的热点数据。

在目前的一线大厂中,这也是最常用的缓存方案,单考单一的缓存方案往往难以撑住很多高并发的场景。

2.4 缓存五问题:缓存雪崩 + 缓存穿透 + 缓存击穿 + 缓存更新 + 数据不一致

三者区别:
缓存雪崩:缓存集体失效,这是后端人员的自己的问题,与网络攻击无关;
缓存穿透:无效id数据,id为负数或id非常大
缓存击穿:热点key,有效id数据

2.4.1 缓存雪崩问题

缓存雪崩
定义:指大量缓存同一时间段集体失效,或者缓存整体不能提供服务,导致大量的请求全部到达数据库 对数据CPU和内存造成巨大压力,严重的会造成数据库宕机。因此而形成的一系列连锁反应造成整个系统奔溃。
【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第2张图片
解决方式
第一,保证缓存的高可用,使用主从模式和集群模式来尽量保证缓存服务的高可用
优点:使用redis的集群模式,即使个别redis节点下线,缓存还是可以用。一般稍微大点的公司还可能会在多个机房部署Redis。这样即使某个机房突然停电,或者光纤又被挖断了,这时候缓存还是可以使用。
Redis集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题,单个服务都是对应的单个Redis分片,是为了方便数据的管理,但是也同样有了可能会失效这样的弊端,失效时间随机是个好策略。
第二,使用多级缓存(本地缓存 + 分布式缓存,推荐方式)。
优点:不同级别缓存时间过时时间不一样,即使某个级别缓存过期了,还有其他缓存级别 兜底。比如我们Redis缓存过期了,我们还有本地缓存。这样的话即使没有命中redis,有可能会命中本地缓存。
第三,缓存永不过期。
优点:缓存永不过期,就不会发生缓存雪崩。
缺点:会浪费更多的存储空间,一般应该也不会推荐这种做法。
应用:电商首页或特别热门的页面,理由:电商首页或特别热门的页面,访问量太大了,一定不能缓存失效,即使加一点存储空间也可以原谅。
第四,使用随机过期时间(推荐方式)
优点:为每一个key都合理的设计一个过期时间(金手指:在缓存时使用固定时间加上一个小的随机数),这样可以避免大量的热点key在同一时刻集体失效;、
setRedis(Key,value,time + Math.random() * 10000);
第五,异步重建缓存。
优点:缓存永不过期,就不会发生缓存雪崩。
缺点:需要维护每个key的过期时间,定时去轮询这些key的过期时间。例如一个key的value设置的过期时间是30min,那我们可以为这个key设置它自己的一个过期时间为20min。所以当这个key到了20min的时候我们就可以重新去构建这个key的缓存,同时也更新这个key的一个过期时间。
第六,使用快速失败的hystrix熔断策略,减少 DB 瞬间压力
使用快速失败的hystrix熔断策略,减少 DB 瞬间压力

【缓存雪崩业务场景1】定时任务导致到缓存雪崩
目前电商首页以及热点数据都会去做缓存 ,一般缓存都是定时任务去刷新,或者是查不到之后去更新的,定时任务刷新就有一个问题。
举个简单的例子:如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能DBA都没反应过来就直接挂了。此时,如果没用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。这就是我理解的缓存雪崩。
即使实在DB层面,也会采取措施,不允许这么大的QPS直接打DB去,比如分库分表,大表分表可能还还算能顶,但是跟用了Redis的差距还是很大
同一时间大面积失效,那一瞬间Redis跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的,你想想如果打挂的是一个用户服务的库,那其他依赖他的库所有的接口几乎都会报错,如果没做熔断等策略基本上就是瞬间挂一片的节奏。
解决方式:电商首页或特别热门的页面设置为缓存永不失效,理由:电商首页或特别热门的页面,访问量太大了,一定不能缓存失效,即使加一点存储空间也可以原谅。

【缓存雪崩业务场景2】如果有大量的key需要设置同一时间过期,一般需要注意什么?
问题:如果大量的key过期时间设置的过于集中,到过期的那个时间点,Redis可能会出现短暂的卡顿现象,严重的话会出现缓存雪崩。
解决:我们一般需要在时间上加一个随机值,使得过期时间分散一些。
实际应用:电商首页经常会使用定时任务刷新缓存,可能大量的数据失效时间都十分集中,如果失效时间一样,又刚好在失效的时间点大量用户涌入,就有可能造成缓存雪崩

2.4.2 缓存穿透问题

缓存穿透(电商:伪造不存在的订单号)
定义:指查询一个不存在的数据,每次通过接口或者去查询数据库都查不到这个数据,比如黑客的恶意攻击,比如知道一个订单号后,然后就伪造一些不存在的订单号,然后并发来请求你这个订单详情。这些订单号在缓存中都查询不到,然后会导致把这些查询请求全部打到数据库或者SOA接口。这样的话就会导致数据库宕机或者你的服务大量超时。这种查询不存在的数据就是缓存穿透。小点的单机系统,基本上用postman就能搞死,比如9.9块钱的阿里云服务

【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第3张图片
解决这个问题可以从以下方面入手:
第一,nginx层:这样可以防止攻击用户反复用同一个id暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的,那网关层Nginx进行配置,可以让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑
第二,controller层校验 + 缓存空值
2.1 在controller层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。
2.2 缓存空值(步骤流程):
对于这些不存在的请求,
第1次查询,从缓存取不到的数据,在数据库中也没有取到,在缓存中将对应Key的Value对写为null;
第2-n次查询,如果后续这个请求还是原值,redis直接返回,从而避免相同 ID 再次访问 DB。
第2-n次查询,如果后续这个请求有新值了,把原来缓存的空值删除掉(所以一般过期时间可以稍微设置的比较短,如30秒,设置太长会导致正常情况也没法使用)。
缺点:可能导致缓存中存储大量无用数据。
第三,固定数据放到缓存中,缓存找不到直接返回
这种方式的话要根据自己的实际业务来进行选择。比如固定的数据,一些省份信息或者城市信息,可以全部缓存起来,对于这些省份和城市信息,都在缓存里面,根本不用访问数据库,缓存找不到,直接返回,避免缓存穿透攻击。
第四,使用布隆过滤器(BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题)
查询缓存之前先去布隆过滤器查询下这个数据是否存在。如果数据不存在,然后直接返回空。这样的话也会减少底层系统的查询压力。原理:使用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。

金手指:后端程序不要相信前端调用或后端服务调用传递的参数(分页为例)
比如你提供了API接口出去,你有这几个参数,那我觉得作为被调用方,任何可能的参数情况都应该被考虑到,做校验,因为你不相信调用你的人,你不知道他会传什么参数给你。
举个简单的例子,你这个接口是分页查询的,但是你没对分页参数的大小做限制,调用的人万一一口气查 Integer.MAX_VALUE 一次请求就要你几秒,多几个并发你不就挂了么?是公司同事调用还好大不了发现了改掉,但是如果是黑客或者竞争对手呢?在你双十一当天就调你这个接口会发生什么,就不用我说了吧。这是之前的Leader跟我说的,我觉得大家也都应该了解下。

2.4.3 缓存击穿问题

缓存击穿
定义:是指缓存里面的一个热点key(拼多多的五菱宏光神车的秒杀)在某个时间点过期。针对于这一个key有大量并发请求过来然后都会同时去数据库请求数据,瞬间对数据库造成巨大的压力(定义2某个热点数据失效时,大量针对这个数据的请求会穿透到数据源)。
解决方式:
第一,缓存永不过期。
优点:缓存永不过期,就不会发生缓存击穿;
缺点:会浪费更多的存储空间,一般应该也不会推荐这种做法。
第二,使用随机退避方式
使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
第三,异步重建缓存
优点:缓存永不过期,就不会发生缓存击穿。
缺点:这样的话需要维护每个key的过期时间,定时去轮询这些key的过期时间。例如一个key的value设置的过期时间是30min,那我们可以为这个key设置它自己的一个过期时间为20min。所以当这个key到了20min的时候我们就可以重新去构建这个key的缓存,同时也更新这个key的一个过期时间。
第四,互斥锁(金手指:互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力)
前提条件:只能针对于同一个key的情况下,比如你有100个并发请求都要来取A的缓存,这时候我们可以借助redis分布式锁来构建缓存,让只有一个请求可以去查询DB其他99个(没有获取到锁)都在外面等着,等A查询到数据并且把缓存构建好之后其他99个请求都只需要从缓存取就好了。原理就跟我们java的DCL(double checked locking)思想有点类似。
【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第4张图片
互斥锁代码实现:
【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第5张图片

2.4.4 缓存更新问题(四种缓存更新方式 + 缓存数据源:DB和远程服务)

四种缓存更新方式
缓存更新问题是决定在使用缓存时就该考虑的问题,一般的缓存更新主要有以下几种更新策略:
1、先更新缓存,再更新数据库
2、先更新数据库,再更新缓存
3、先删除缓存,再更新数据库
4、先更新数据源库,再删除缓存(推荐)
至于选择哪种更新策略的话,没有绝对的选择,可以根据自己的业务情况来选择适合自己的不过一般
推荐的话是选择 「先更新数据源库,再删除缓存」。

缓存数据源:DB和远程服务
缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。
第一,当数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。这种场景下,更新的方式可以是主动更新。
第二,当数据源不是 DB 而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。这种场景下,更新方式可以选择失效更新,即 key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。
附加:远程服务更新redis的问题:如果依赖的远程服务在更新时出现异常,则会导致数据不可用。
解决1:使用异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。这样就避免了失效瞬间的空窗期。
解决2:另外还有一种纯异步更新方式,定时对数据进行分批更新。
所以,实际使用时可以根据业务场景选择更新方式。

你了解最经典的KV、DB读写模式么?
回答:最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern
(1)读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
(2)写的时候,先更新数据库,然后再删除缓存。

缓存更新:为什么写的时候,是删除缓存,而不是更新缓存?
第一,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值,而是数据库中取出来并适当计算的值。比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
第二,更新缓存的代价有时候是很高的,所以使用懒加载,需要用到的时候才更新缓存。
第三,避免频繁写而不是频繁读:假设修改数据库的时候,每次将其对应的缓存更新一份,对于比较复杂的缓存数据计算的场景,频繁修改一个缓存涉及的多个表,缓存也频繁更新,但是,这个缓存不会被被频繁读(频繁写redis但是不频繁读redis,写了就没意义,消耗性能,所以等到需要的时候再写redis,懒加载)。比如:一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。、
第四,懒加载,用到才更新缓存:如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。选择删除缓存,而不是更新缓存,就是一个 Lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 Mybatis,Hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 List,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。

2.4.5 缓存数据不一致问题

数据不一致问题
只要使用缓存,就要考虑如何面对数据不一致问题。
定义:缓存不一致产生的原因一般是主动更新失败,例如更新 DB 后,更新 Redis 因为网络原因请求超时;或者是异步更新失败导致。
解决:
(1)如果服务对耗时不是特别敏感可以增加重试;
(2)如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。

缓存与数据库的数据一致性问题?处理方式?
第一,类比
很简单,在学习计算机组成原理的时候,在cpu和内存中加上了一个cache,保证内存和cache数据一致性就是两种方式,写直达法和写回法。
现在,在后端和mysql中加一个redis,就要保证redis和mysql中的数据一致性,只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
类比,cache中存放内存中部分数据,redis中存放数据库中部分数据。
两个类比统一,mysql和redis绑定在一起,多个后端系统来访问它们;互斥变量和服务端程序绑定在一起,多个线程来访问它们。
mysql和redis数据一致性问题/cache和内存数据一致性问题:写直达法和写回法;
多个系统竞争redis问题/多个线程竞争互斥变量问题:线程互斥+线程通信
第一,如果严格要求 “缓存+数据库” 必须保持一致性的话,使用:读请求和写请求串行化,串到一个内存队列里去。串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。解释:把一些列的操作都放到队列里面,顺序肯定不会乱,但是并发高了,这队列很容易阻塞,反而会成为整个系统的弱点,瓶颈。
第二,如果允许缓存可以稍微的跟数据库偶尔有不一致的话,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,不需要这样使用。

三、限流(服务限流,表示处理的对象是服务,包括限流四规则 + 限流四实现)

3.1 限流

限流定义
限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。

为什么要限流?一句话小结:为了保证系统的稳定运行。
假设我们一个系统一小时之最多只能处理10000个请求,但是一小时流量突增10倍,这突增的流量我们如果不进行限制的话,任由它直接进入系统的话,是不是直接会把我们的系统弄瘫痪,就无法对外提供服务了。
金手指:高并发的五种措施,缓存、限流、降级、熔断、隔离都是保护系统的稳定运行

3.2 限流四规则

限流四个规则,
(要记忆,语言已组织好)既然要限流,就是要允许一部分请求进入,阻止另外一部分请求进入,那么根据什么规则来筛选进入的请求和被拒绝的请求呢(即提供什么规则决定谁去好大学,谁去差大学,就是高考)?提供四种:
1、后端不做任何干涉,完全交给不确定的网络,先达到的请求先处理,后达到的请求后处理,达到阈值直接拒绝服务;
2、后端对服务分级,对于核心服务的请求就处理,对于非核心的请求的服务不处理,又称服务的主动降级;
3、后端所有请求都处理,但是放到延迟队列中,一点一点的处理。
4、后端对用户分级,对于重要用户的请求优先处理,对于普通用户的请求普通处理,这就是限流规则。

第一种限流,达到阈值直接拒绝服务,实现限流,最简单的限流,这种限流方式实现简单,但是要求阈值设置合理,
这个是最最简单粗暴的做法了,直接把请求直接拒绝掉。比如早高峰坐地铁的时候,直接让进入1000个人,剩下多出来的人不让坐地铁了。直接把入站口给关闭了。
第二种限流,服务降级,实现限流,核心:服务分级
将系统的所有功能服务进行一个分级,当系统出现问题,需要紧急限流时,可将不是那么重要的功能进行降级处理,停止服务,这样可以释放出更多的资源供给核心功能的去用。比如:有一个功能新用户注册完,要给用户发送多少优惠券。这时候服务降级的话就可以直接把送券服务关掉,让服务快速响应,提高系统处理能力。
第三种限流,请求放到队列种,延迟处理,实现限流,核心:队列
把请求全部放入到队列中,真正处理的话,就从队列里面依次去取,这样的话流量比较大的情况可能会导致处理不及时,会有一定的延时。双十一零点我们付款的时候,去查询订单的状态是不是也会有一定的延时,不像在平时付完款订单状态就变成了付款状态。
第四种限流,特权处理,实现限流,核心:用户分级
这个模式需要将用户进行分类,通过预设的分类,让系统优先处理需要高保障的用户群体,其它用户群的请求就会延迟处理或者直接不处理。我们去银行办理业务的时候是不是也会经常需要排队,但是是不是经常会VIP用户、什么白金卡用户,直接不需要排队,直接一上来就可以办理业务,还优先处理这些人的业务。是不是特羡慕这些人,哎 羡慕也没办法谁叫人家有钱咧。
金手指:第二种和第四种都是分级,有什么不同?
回答:分级的对象不同。第二种是服务分级,即对事不对人,对不重要的服务限流;第四种是用户分级,即对人不对事,对不重要的用户限流。

3.3 限流四实现

三种限流组件、三种限流实现方式
第一种,计数器方法(达到阈值直接拒绝访问)
这是最简单的限流算法了,系统里面维护一个计数器,来一个请求就加1,请求处理完成就减1,当计数器大于指定的阈值,就拒绝新的请求。是通过全局的总求数于设置的阈值来达到限流的目的。通常应用在池化技术上面比如:「数据库连接池、线程池」等中应用。这种方式的话限流不是「平均速率」的。扛不住突增的流量。
第二种,滑动窗口算法
一种常见的流量控制技术,用来改善吞吐量的技术
【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第6张图片
第三种,漏桶算法
【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第7张图片
上图,水是可以持续流入漏桶里面的(即所有请求一定会经过漏桶的过滤),底部也是匀速的流出,如果漏桶的水超过桶的大小就会发生益出(即 流入的速率大于底部流出的速率 + 持续一段时间后)。
重点:两个速率:
流入速率即实际的用户请求速率或压力测试的速率,流出速率即服务端处理速率。
一般来说,流出速度是固定的,即不管你请求有多少,速率有多快,我反正就这么个速度处理。当然,特殊情况下,需要加快速度处理,也可以动态调整流出速率。
第四种,令牌桶
【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第8张图片
上图解释:如果令牌的数量超过里桶的限制的话,令牌就会溢出,这时候就直接舍弃多余的令牌。
每个请求过来必须拿到桶里面拿到了令牌才允许请求(拿令牌的速度是不限制的,这就意味着如果瞬间有大量的流量请求进来,可以短时间内拿到大量的令牌),拿不到令牌的话直接拒绝。这个令牌桶的思想是不是跟我们java里面的「Semaphore」 有点类似。Semaphore 是拿信号量,用完了就还回去。但是令牌桶的话,不需要还回去,因为令牌会定时的补充。令牌桶算法我们可以通过
Google开源的guava包创建一个令牌桶算法的限流器。
金手指:令牌桶和漏桶不同点:令牌桶新增了一个匀速生产令牌的中间人以恒定的速度往桶里面放令牌,上面的漏桶,流入速率根本不控制,用户请求压力直接达到漏桶来(令牌桶这个匀速流入速率和mq对于mysql请求量的控制很像)。
以上是三种限流组件,大家可以根据这个思想然后去实现各种各样的限流组件。

金手指:任何限流组件都要设置阈值
第一,不管是哪种限流方法,直接拒绝也要,漏桶、令牌桶也好,限流算法里面一定有一个阈值(解释:直接拒绝要设置阈值,漏桶令牌桶要设置桶大小),这个阈值设置为多少是不是比较难。阈值设置过大的话,服务可能扛不住,阈值设置小了会把用户请求给误杀,资源没有得到最大的一个利用。
第二,任何限流组件都要设置阈值,这是限流和其他两种保护系统稳定运行的方式(降级、熔断)的最大区别,即限流一定要好设置阈值。

四、降级(即服务降级,表示处理的对象是服务,这里指主动降级)

4.1 服务降级的定义

服务降级的定义:
服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
来源于百度百科。这些话看起来是不是不是很好理解,那么我们可以举个栗子:双十一的时候,我们买东西是不是都不允许修改购物地址,不允许发起退货,不允许退款还有很多服务都不可以用,只允许用户选择商品加入购物车付钱。那天只有一个目的就是让一些不是很重要的服务所占用的cpu资源都让出来,给购物,付款这样的核心服务。这样的话,用户付款的速度就会越来越快,毕竟前多少名支付的用户是有免单机会的(大家都会挤在0点那一刻去付款)。服务降级主要用于当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,将一些 不重要 或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用。 降级就是为了解决资源不足和访问量增加的矛盾。

4.2 服务降级五种方式

服务降级方式
第一,延迟服务:定时任务处理、或者mq延时处理。比如新用户注册送多少优惠券可以提示用户优惠券会24小时到达用户账号中,我们可以选择再凌晨流量较小的时候,批量去执行送券。
第二,被动降价,前端页面降级:页面点击按钮全部置灰,或者页面调整成为一个静态页面显示“系统正在维护中,。。。。”。
第三,主动降级,关闭非核心服务:比如电商关闭推荐服务、关闭运费险、退货退款等。保证主流程的核心服务下单付款就好。
第四,写降级:比如秒杀抢购,我们可以只进行Cache的更新返回,然后通过mq异步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
第五,读降级:比如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。

五、熔断(即服务熔断,表示处理的对象是服务,就是被动降级)

5.1 服务雪崩

服务雪崩
说到服务熔断我们就得先了解下什么是服务雪崩。雪崩效应好比就是蝴蝶效应,说的都是一个小因素的变化,却往往有着无比强大的力量,以至于最后改变整体结构、产生意想不到的结果。 多个微服务之间调用的时候,比如A服务调用了B服务,B服务调用了C服务,然后C服务由于机器宕机或者网略故障, 然后就会导致B服务调用C服务的时候超时,然后A服务调用B服务也会超时,最终整个链路都不可用了,导致整个系统不可用就跟雪蹦一样。
【高并发】高并发五个利器(缓存、限流、降级、熔断、隔离)_第9张图片

5.2 服务雪崩产生的原因

雪崩效应产生的几种场景
1、突增流量:比如一大波爬虫,或者黑客攻击等。
2、程序bug:代码死循环,或者资源未释放等。
3、硬件原因:机器宕机、机房断电、光纤被挖断等。

5.3 熔断机制是应对雪崩效应的一种微服务链路保护机制

熔断机制是应对雪崩效应的一种微服务链路保护机制,在互联网系统中当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,暂时不再继续调用目标服务,直接快速返回失败标志,快速释放资源。如果目标服务情况好转则恢复调用。

六、面试金手指

6.1 限流四规则 + 限流四实现

限流四个规则,
(要记忆,语言已组织好)既然要限流,就是要允许一部分请求进入,阻止另外一部分请求进入,那么根据什么规则来筛选进入的请求和被拒绝的请求呢(即提供什么规则决定谁去好大学,谁去差大学,就是高考)?提供四种:
1、后端不做任何干涉,完全交给不确定的网络,先达到的请求先处理,后达到的请求后处理,达到阈值直接拒绝服务;
2、后端对服务分级,对于核心服务的请求就处理,对于非核心的请求的服务不处理,又称服务的主动降级;
3、后端所有请求都处理,但是放到延迟队列中,一点一点的处理。
4、后端对用户分级,对于重要用户的请求优先处理,对于普通用户的请求普通处理,这就是限流规则。

限流四实现
1、第一种,计数器方法(达到阈值直接拒绝访问)
2、第二种,滑动窗口算法,用于改善吞吐量技术
3、第三种,漏桶算法:流入速率即实际的用户请求速率或压力测试的速率,流出速率即服务端处理速率。如果漏桶的水超过桶的大小就会发生益出(即 流入的速率大于底部流出的速率 + 持续一段时间后)。
4、第四种,令牌桶
金手指:令牌桶和漏桶不同点:令牌桶新增了一个匀速生产令牌的中间人以恒定的速度往桶里面放令牌,上面的漏桶,流入速率根本不控制,用户请求压力直接达到漏桶来(令牌桶这个匀速流入速率和mq对于mysql请求量的控制很像)。

金手指:任何限流组件都要设置阈值
第一,不管是哪种限流方法,直接拒绝也要,漏桶、令牌桶也好,限流算法里面一定有一个阈值(解释:直接拒绝要设置阈值,漏桶令牌桶要设置桶大小),这个阈值设置为多少是不是比较难。阈值设置过大的话,服务可能扛不住,阈值设置小了会把用户请求给误杀,资源没有得到最大的一个利用。
第二,任何限流组件都要设置阈值,这是限流和其他两种保护系统稳定运行的方式(降级、熔断)的最大区别,即限流一定要好设置阈值。

6.2 五种措施小结

起手式,五种措施的总体比较,拉高逼格
第一,五种措施目的相同:都是牺牲部分用户的体验换来服务器的安全,保护系统稳定运行(即
只要保证数据库绝对不会死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次);
第二,接上面,为了达到系统稳定运行的目的
2.1 三种措施终表现类似:限流降级熔断,为了达到维护系统稳定运行的目的,最终让用户体验到的是某些功能暂时不可达或不可用;
2.2 其他两种方式,为了达到系统稳定运行的目的,缓存使用多级缓存(本地缓存+分布式缓存)加快速度达到这个目的;隔离通过设计低耦合后端系统达到这个目的,mysql分库低耦合,mq分业务低耦合,redis集群低耦合,微服务框架低耦合,nginx业务集群低耦合。
第三,接上面,三种措施粒度一般都是服务级别:限流降级熔断,粒度一般都是服务级别
隔离在设计上也是服务级别的,服务与服务之间隔离,构建微服务系统,对于其他资源,mysql、mq redis nginx 都是业务模块级别隔离,也可以细粒度隔离,放到不同服务器上,比如mysql业务模块的库级别隔离,可以更加细粒度表级别隔离。

第二,金手指(缓存 + 限流 + 降级 VS 熔断 + 隔离) 一句话小结
缓存 + 限流 + 降级 立足与单个服务,是为了保证单个服务,单个热点服务不要发生宕机;
熔断和隔离 立足与服务间关系,是保证单个服务发生宕机后,不要影响或尽可能小的影响其他服务;进一步阐述熔断和隔离,隔离是服务宕机前,系统启动前,服务设计和资源设计,一种设计思想,服务间低耦合的设计思想,包括资源和调用的低耦合,mysql分库低耦合,mq分业务低耦合,redis集群低耦合,微服务框架低耦合,nginx业务集群低耦合;熔断是服务宕机后,该服务及调用该服务的服务,熔断,下一请求快速失败,如hystrix。

第三,限流 和其他四者区别
定义:限流:根据一定规则阻挡,阈值是事先设定好的。
不同点:限流,被阻挡的请求没有被抛弃了;降级(主动降级),被降级的服务暂时无法访问。
被动降级即熔断,被阻挡的请求直接熔断失败。
缓存和隔离与具体实际请求无关

第三,附:限流和降级
限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走被动降级,即熔断,可以返回一些默认的值,或者友情提示,或者空白的值。例如,对于某明星爆出什么事情,你发现你去微博怎么刷都空白界面,但是有的人又直接进了,你多刷几次也出来了,现在知道了吧,那是做了降级。

第四,金手指 熔断(熔断和四种区别)
第一,缓存、限流、降级、隔离是在设计为了尽量避免服务发生异常,熔断是指服务发生异常后,将损害降低到最小。
第二,自治性要求很高: 熔断模式一般都是服务基于策略的自动触发,其他的,缓存、限流、降级、隔离是人工干预。
第三,触发原因不同:熔断一般是某个具体服务(下游服务)故障引起,而缓存、限流、降级、隔离一般是从整体负荷考虑;
第四,管理目标的层次不同:熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而缓存、限流、降级、隔离一般需要对业务有层级之分(比如降级一般是从最外围服务开始)。

主动降级和被动降级
定义:
降级是主动降级,降低非核心服务级别;
熔断是被动降级,降低当前宕机服务级别,让其快速失败
一个例子囊括主动降级和被动降级
主动降价:在双十一,淘宝流量剧增,停用一部分服务,比如退款拒绝,退款义务不是那么紧急,就是暂时关闭退款服务,等到以后在开放
限流和熔断(被动降级):比如下单延迟,下单接口其实没挂,牺牲部分用户体验,保住服务器,你多点几下是可以成功的,等流量高峰过去了,所有的用户全部都恢复正常访问,服务器也没啥事。

七、小结

高并发五个利器(缓存、限流、降级、熔断、隔离),结束了。

你可能感兴趣的:(Java学习)