前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

1、缓存

缓存分为本地缓存和分布式缓存两种(分级缓存就是应用缓存+分布式缓存):

分布式缓存(即进程间缓存)如redis、memcached等,

本地缓存(即进程内缓存)如ehcache、GuavaCache、Caffeine等。

1.1、缓存特征

第一特征,命中率

定义:命中率=命中数/(命中数+没有命中数)当某个请求能够通过访问缓存而得到响应时,称为缓存命中。缓存命中率越高,缓存的利用率也就越高。

第二特征,最大空间

定义:最大空间表示缓存中可以容纳最大元素的数量。当缓存存放的数据超过最大空间时,就需要根据淘汰算法来淘汰部分数据存放新到达的数据。

第三特征,淘汰算法

定义:缓存的存储空间有限制,当缓存空间被用满时,要求保证在稳定服务的同时有效提升命中率,这就由缓存淘汰算法来处理,设计适合自身数据特征的淘汰算法能够有效提升缓存命中率。

1.2、3种淘汰算法

三种淘汰算法:业务驱动技术,如何选择淘汰算法

1.2.1、FIFO(first in first out) 先进先出

定义:最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据

比较的对象:策略算法主要比较缓存元素的创建时间。

业务场景:适用于保证高频数据有效性场景,优先保障最新数据可用。

实现代码:队列

1.2.2、LFU(less frequently used)「最少使用」

定义无论是否过期,仅根据元素的被使用次数判断,清除使用次数较少的元素释放空间

比较的对象:策略算法主要比较元素的hitCount(命中次数)。

业务场景:适用于保证高频数据有效性场景。

实现代码:。(Map保存key的使用个数)

1.2.3、LRU(least recently used)「最近最少使用」

定义无论是否过期,仅根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间

比较的对象:策略算法主要比较元素最近一次被get使用时间。

业务场景:比较适用于热点数据场景,优先保证热点数据的有效性。

实现代码:看Redis那篇文章

1.3、缓存业务

业务驱动技术,如何设置过期时间,以一个飞机票订单为例

案例一:对于已出行、或者已经取消的订单我们基本上是不会去管的(因为订单状态已经终止了,不会再有对于订单状态的写请求),

1、对于订单读操作:实时性要求没那么高,从缓存中拿

所以,对于这种订单我们设置的过期时间是不是就可以久一点,比如7天或者30天

案例二:对于未出行即将起飞的订单,这时候顾客是不是就会频繁的去刷新订单看看,看看有没有晚点什么的,或者登机口是在哪。(一般来说,读多写少。)

1、 对于读操作:实时性要求没那么特别高,从缓存中拿,但是也相对来说实时性比较高,所以过期时间设置成相对比较短的

2 、对于订单写操作,需要更改订单的状态的(比如退票、改签),可以直接不走缓存,直接查询并修改数据库。

所以,对于这种实时性要求比较高的订单我们过期时间还是要设置的比较短的

1.4、缓存分类

1.4.1、本地缓存

本地缓存定义:应用和缓存都在同一个进程里面,

本地缓存优点:获取缓存数据的时候纯内存操作,没有额外的网络开销,速度非常快。

本地缓存缺点:本地缓存与业务系统耦合在一起,应用之间无法直接共享缓存的内容。需要每个应用节点单独的维护自己的缓存。每个节点都需要一份一样的缓存,对服务器内存造成一种浪费。本地缓存机器重启、或者宕机都会丢失。

本地缓存存放不可变数据:它适用于缓存一些应用中基本不会变化的数据,比如(国家、省份、城市等),一般可以在应用启动的时候,把需要的数据加载到系统中。

1.4.2、分布式缓存

分布式缓存定义:与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。

分布式缓存举例:常见的分布式缓存有redis、MemCache等。

分布式缓存的应用

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

1.4.3、多级缓存

多级缓存(本地缓存+分布式缓存) 为了平衡本地缓存和分布式缓存,实际业务中一般采用多级缓存,在目前的一线大厂中,这也是最常用的缓存方案,单考单一的缓存方案往往难以撑住很多高并发的场景。

1、本地缓存中只保存访问频率最高的部分热点数据

2、分布式缓冲中保存其他的热点数据。

1.5、更新缓存的方式

广播订阅mq消息更新 > 定时变量更新 > 定时全量更新。

1.5.1、定时全量更新(实时性不高

全量,这个很好理解。 就是每天定时(避开业务高峰期)或者周期性全量把数据从一个地方拷贝到另外一个地方;

1、可以采用直接全部覆盖(使用“新”数据覆盖“旧”数据);

2、或者走更新逻辑(覆盖前判断下,如果新旧不一致,就更新);

1.5.2、定时增量更新(实时性不高)

增量的基础是全量,就是你要使用某种方式先把全量数据拷贝过来,然后再采用增量方式同步更新。就是指抓取某个时刻(更新时间)或者检查点(checkpoint)以后的数据来同步,不是无规律的全量同步。

在应用中起一个定时任务(「ScheduledExecutorService」、「TimerTask」等),让它每隔多久去加载变更(数据变更之后可以修改数据库最后修改的时间,每次查询变更数据的时候都可以根据这个最后变更时间加上半小时大于当前时间的数据)的数据重新到缓存里面来。

1.5.3、广播订阅mq消息(实时性高)

无论是定时增量更新还是定时全量更新,实时性都不高,所以有了广播订阅mq消息更新。

如果对实时性有要求的话,使用广播订阅mq消息更新。一旦有数据更新mq会把更新数据推送到每一台机器,实时性好,但是实现起来较为复杂。

2、缓存问题

2.1、缓存雪崩问题

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

和上面的理解可能不一样不支持rehash导致的不可用,一般是由于缓存节点不可用,请求穿透到db导致db也过载,最终整个系统不可用;

支持rehash导致的不可用,大多跟流量洪峰有关,流量洪峰到达,引发部分缓存节点过载Crash,然后因rehash扩散到其他缓存节点,最终整个缓存体系异常。

业务场景:

缓存雪崩的业务场景并不少见,微博、Twitter 等系统在运行的最初若干年都遇到过很多次。比如,微博最初很多业务缓存采用一致性 Hash+rehash 策略,在突发洪水流量来临时,部分缓存节点过载 Crash 甚至宕机,然后这些异常节点的请求转到其他缓存节点,又导致其他缓存节点过载异常,最终整个缓存池过载。另外,机架断电,导致业务缓存多个节点宕机,大量请求直接打到 DB,也导致 DB 过载而阻塞,整个系统异常。最后缓存机器复电后,DB 重启,数据逐步加热后,系统才逐步恢复正常。

2.2.1、解决方案

2.2.1.1、主从模式和集群模式

保证缓存的高可用,使用主从模式和集群模式来尽量保证缓存服务的高可用

优点:使用redis的集群模式,即使个别redis节点下线,缓存还是可以用。一般稍微大点的公司还可能会在多个机房部署Redis。这样即使某个机房突然停电,或者光纤又被挖断了,这时候缓存还是可以使用。

2.2.1.2、使用多级缓存

使用多级缓存(本地缓存 + 分布式缓存,推荐方式)。

优点:不同级别缓存时间过时时间不一样,即使某个级别缓存过期了,还有其他缓存级别 兜底。比如我们Redis缓存过期了,我们还有本地缓存。这样的话即使没有命中redis,有可能会命中本地缓存。

2.2.1.3、缓存永不过期

优点:缓存永不过期,就不会发生缓存雪崩。

缺点:会浪费更多的存储空间,一般应该也不会推荐这种做法。

应用:电商首页或特别热门的页面,理由:电商首页或特别热门的页面,访问量太大了,一定不能缓存失效,即使加一点存储空间也可以原谅

2.2.1.7、缓存副本

对缓存增加多个副本,缓存异常或请求 miss 后,再读取其他缓存副本,而且多个缓存副本尽量部署在不同机架,从而确保在任何情况下,缓存系统都会正常对外提供服务

2.2.1.4、使用随机过期时间

为每一个key都合理的设计一个过期时间(在缓存时使用固定时间加上一个小的随机数) ,这样可以避免大量的热点key在同一时刻集体失效

2.2.1.5、异步重建缓存

优点:缓存永不过期,就不会发生缓存雪崩。

缺点:需要维护每个key的过期时间,定时去轮询这些key的过期时间。例如一个key设置的过期时间是30min,那我们可以为这个key设置它的一个副本自己的一个过期时间为20min。所以当这个key到了20min的时候我们就可以重新去构建这个key的缓存,同时也更新这个key的一个过期时间。

2.2.1.6、熔断降级

对缓存体系进行实时监控,当请求访问的速度比超过阀值时,及时报警,通过机器替换、服务替换进行及时恢复; 也可以通过各种自动故障转移策略,自动关闭异常接口、停止边缘服务、停止部分非核心功能措施,确保在极端场景下,核心功能的正常运行。

2.2.1.7、DB读写开关

对业务 DB 的访问增加读写开关,当发现 DB 请求变慢、阻塞,慢请求超过阀值时,就会关闭读开关,部分或所有读 DB 的请求进行 failfast 立即返回,待 DB 恢复后再打开读开关,如下图。

2.2、缓存穿透

指查询一个不存在的数据,每次通过接口或者去查询数据库都查不到这个数据,比如黑客的恶意攻击,比如知道一个订单号后,然后就伪造一些不存在的订单号,然后并发来请求你这个订单详情。这些订单号在缓存中都查询不到,然后会导致把这些查询请求全部打到数据库或者SOA接口。这样的话就会导致数据库宕机或者你的服务大量超时。这种查询不存在的数据就是缓存穿透。

2.2.2、解决方案

2.2.2.1、nginx层

我们要知道正常用户是不会在单秒内发起这么多次请求的,那网关层Nginx进行配置,可以让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑

2.2.2.3、布隆过滤器

**用布隆过滤器(BloomFilter 的特点是存在性检测, **

如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题)

查询缓存之前先去布隆过滤器查询下这个数据是否存在。如果数据不存在,然后直接返回空。这样的话也会减少底层系统的查询压力。原理:使用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。

构建一个 BloomFilter 缓存过滤器,记录全量数据,这样访问数据时,可以直接通过 BloomFilter 判断这个 key 是否存在,如果不存在直接返回即可,根本无需查缓存和 DB。但是 BloomFilter 要缓存全量的 key,这就要求全量的 key 数量不大,10 亿条数据以内最佳,因为 10 亿 条数据大概要占用 1.2GB 的内存。

2.2.2.4、缓存空值(步骤流程)

第1次查询,从缓存取不到的数据,在数据库中也没有取到,在缓存中将对应Key的Value对写为null;

缺点: 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除

2.2.2.5、分布式锁

只能针对于同一个key的情况下,其他没有获取到锁的,就等待一段时间,如果数据库查不到,就继续维持这个锁的时间

比如你有100个并发请求都要来取A的缓存,这时候我们可以借助redis分布式锁来构建缓存,让只有一个请求可以去查询DB其他99个(没有获取到锁)都在外面等着,等A查询到数据并且把缓存构建好之后其他99个请求都只需要从缓存取就好了。

2.3、缓存击穿/并发访问

看情况,有时候和缓存穿透解决方案一样

是指缓存里面的一个热点key(拼多多的五菱宏光神车的秒杀)在某个时间点过期。 针对于这一个key有大量并发请求过来然后都会同时去数据库请求数据,瞬间对数据库造成巨大的压力

2.3.1、解决方案

2.3.1.1、缓存永不过期

优点:缓存永不过期,就不会发生缓存雪崩。

缺点:会浪费更多的存储空间,一般应该也不会推荐这种做法。

2.2.1.2、分布式锁

只能针对于同一个key的情况下,其他没有获取到锁的,就等待一段时间,如果数据库查不到,就继续维持这个锁的时间

比如你有100个并发请求都要来取A的缓存,这时候我们可以借助redis分布式锁来构建缓存,让只有一个请求可以去查询DB其他99个(没有获取到锁)都在外面等着,等A查询到数据并且把缓存构建好之后其他99个请求都只需要从缓存取就好了。原理就跟我们java的DCL(double checked locking)思想有点类似。

2.2.1.3、缓存副本

对缓存数据保持多个备份,即便其中一个备份中的数据过期或被剔除了,还可以访问其他备份,从而减少数据并发竞争的情况。

2.4、 缓存不一致问题

不一致的问题大多跟缓存更新异常有关。比如更新 DB 后,写缓存失败,从而导致缓存中存的是老数据。另外,如果系统采用一致性 Hash 分布,同时采用 rehash 自动漂移策略,在节点多次上下线之后,也会产生脏数据。缓存有多个副本时,更新某个副本失败,也会导致这个副本的数据是老数据。

2.4.1、解决方案

2.4.1.1、更新失败、队列重试

cache 更新失败后,可以进行重试,如果重试失败,则将失败的 key 写入队列机服务,待缓存访问恢复后,将这些 key 从缓存删除。这些 key 在再次被查询时,重新从 DB 加载,从而保证数据的一致性。

2.4.1.2、缓存时间缩短

缓存时间适当调短,让缓存数据及早过期后,然后从 DB 重新加载,确保数据的最终一致性。

2.4.1.3、分层策略

不采用 rehash 漂移策略,而采用缓存分层策略,尽量避免脏数据产生。

2.5、缓存失效

缓存失效是由于大量key同时过期,请求直接从db加载导致db压力明显上升的问题。

2.5.1、解决方案

2.5.1.2、使用随机过期时间

为每一个key都合理的设计一个过期时间(在缓存时使用固定时间加上一个小的随机数) ,这样可以避免大量的热点key在同一时刻集体失效

2.7、Hot key

Hot key 引发缓存系统异常,主要是因为数十万、数百万的用户同时请求同一个 key,流量集中打在一个缓存节点机器,这个缓存机器很容易被打到物理网卡、带宽、CPU 的极限,从而导致缓存访问变慢、卡顿。

2.7.1、key变多

找到对应的热点 key,将这些热 key 进行分散处理,比如一个热 key 名字叫 hotkey,可以被分散为 hotkey#1、hotkey#2、hotkey#3,……hotkey#n,这 n 个 key 分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的 hotkey,这样就可以把热 key 的请求打散,避免一个缓存节点过载

2.7.1、热key放到本地缓存中

业务端还可以使用本地缓存,将这些热 key 记录在本地缓存,来减少对远程缓存的冲击

2.8、大key和大value

3、限流

假设我们一个系统一小时之最多只能处理10000个请求,但是一小时流量突增10倍,这突增的流量我们如果不进行限制的话,任由它直接进入系统的话,是不是直接会把我们的系统弄瘫痪,就无法对外提供服务了。

(要记忆,语言已组织好)既然要限流,就是要允许一部分请求进入,阻止另外一部分请求进入,那么根据什么规则来筛选进入的请求和被拒绝的请求呢

3.1、解决方案

1、后端不做任何干涉,完全交给不确定的网络,先达到的请求先处理,后达到的请求后处理,达到阈值直接拒绝服务;

2、后端对服务分级,对于核心服务的请求就处理,对于非核心的请求的服务不处理,又称服务的主动降级;(注册,送积分。暂时不给积分)

3、后端所有请求都处理,但是放到延迟队列中,一点一点的处理。(火车票抢票)

4、后端对用户分级,对于重要用户的请求优先处理,对于普通用户的请求普通处理,这就是限流规则。

3.1.1、达到阈值直接拒绝服务,实现限流

第一种限流,达到阈值直接拒绝服务,实现限流,最简单的限流,这种限流方式实现简单,但是要求阈值设置合理, 这个是最最简单粗暴的做法了,直接把请求直接拒绝掉。比如早高峰坐地铁的时候,直接让进入1000个人,剩下多出来的人不让坐地铁了。直接把入站口给关闭了。

3.1.2、服务降级,实现限流,核心:服务分级

将系统的所有功能服务进行一个分级,当系统出现问题,需要紧急限流时,可将不是那么重要的功能进行降级处理,停止服务,这样可以释放出更多的资源供给核心功能的去用。比如:有一个功能新用户注册完,要给用户发送多少优惠券。这时候服务降级的话就可以直接把送券服务关掉,让服务快速响应,提高系统处理能力。

3.1.3、请求放到队列种,延迟处理,实现限流,核心:队列

把请求全部放入到队列中,真正处理的话,就从队列里面依次去取,这样的话流量比较大的情况可能会导致处理不及时,会有一定的延时。双十一零点我们付款的时候,去查询订单的状态是不是也会有一定的延时,不像在平时付完款订单状态就变成了付款状态。

3.1.4、特权处理,实现限流,核心:用户分级

这个模式需要将用户进行分类,通过预设的分类,让系统优先处理需要高保障的用户群体,其它用户群的请求就会延迟处理或者直接不处理。我们去银行办理业务的时候是不是也会经常需要排队,但是是不是经常会VIP用户、什么白金卡用户,直接不需要排队,直接一上来就可以办理业务,还优先处理这些人的业务。是不是特羡慕这些人,哎 羡慕也没办法谁叫人家有钱咧。

3.2、限流实现

任何限流组件都要设置阈值

漏桶算法与令牌桶算法的区别

漏桶算法能够强行限制数据的传输速率。令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。

需要说明的是:在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。

通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。

3.1.1、计数器方法

这是最简单的限流算法了,系统里面维护一个计数器,来一个请求就加1,请求处理完成就减1,当计数器大于指定的阈值,就拒绝新的请求。是通过全局的总求数于设置的阈值来达到限流的目的。通常应用在池化技术上面比如:「数据库连接池、线程池」等中应用。这种方式的话限流不是「平均速率」的。扛不住突增的流量。

1.1.2、滑动窗口

如果对滑动窗口不熟悉可以先了解下TCP的滑动窗口协议。限流中的滑动窗口可以简单理解为,设定的单位时间就是一个窗口,窗口可以分割多个更小的时间单元,随着时间的推移,窗口会向右移动。比如一个接口一分钟限制调用1000次,1分钟就可以理解为一个窗口,可以把1分钟分割为10个单元格,每个单元格就是6秒。

image-20201207153355233

3.1.2、漏桶算法

水是可以持续流入漏桶里面的(即所有请求一定会经过漏桶的过滤),底部也是匀速的流出,如果漏桶的水超过桶的大小就会发生益出(即 流入的速率大于底部流出的速率 + 持续一段时间后)。

一般来说,流出速度是固定的,即不管你请求有多少,速率有多快,我反正就这么个速度处理。当然,特殊情况下,需要加快速度处理,也可以动态调整流出速率

重点:两个速率:

流入速率:即实际的用户请求速率或压力测试的速率,

流出速率:即服务端处理速率。

image-20201207104017780

3.1.3、令牌桶算法

如果令牌的数量超过里桶的限制的话,令牌就会溢出,这时候就直接舍弃多余的令牌。

每个请求过来必须拿到桶里面拿到了令牌才允许请求(拿令牌的速度是不限制的,这就意味着如果瞬间有大量的流量请求进来,可以短时间内拿到大量的令牌),拿不到令牌的话直接拒绝。 令牌桶算法我们可以通过Google开源的guava包创建一个令牌桶算法的限流器。

令牌桶和漏桶不同点:令牌桶新增了一个匀速生产令牌的中间人以恒定的速度往桶里面放令牌,上面的漏桶,流入速率根本不控制,用户请求压力直接达到漏桶来(令牌桶这个匀速流入速率和mq对于mysql请求量的控制很像)。

image-20201207104400806

4、降级

服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。

第二,被动降价,前端页面降级:页面点击按钮全部置灰,或者页面调整成为一个静态页面显示“系统正在维护中,。。。。”。

第三,主动降级,关闭非核心服务:比如电商关闭推荐服务、关闭运费险、退货退款等。保证主流程的核心服务下单付款就好。

第一,延迟服务:定时任务处理、或者mq延时处理。比如新用户注册送多少优惠券可以提示用户优惠券会24小时到达用户账号中,我们可以选择再凌晨流量较小的时候,批量去执行送券。

第四,写降级:比如秒杀抢购,我们可以只进行Cache的更新返回,然后通过mq异步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。

第五,读降级:比如多级缓存模式,如果后端数据库服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。

5、熔断

多个微服务之间调用的时候,比如A服务调用了B服务,B服务调用了C服务,然后C服务由于机器宕机或者网略故障, 然后就会导致B服务调用C服务的时候超时,然后A服务调用B服务也会超时,最终整个链路都不可用了,导致整个系统不可用就跟雪蹦一样。

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

ContactAuthor