架构师训练营学习总结——缓存与消息队列【第五周】

用户头像
王海
关注
发布于: 2020 年 07 月 06 日

一、分布式缓存架构

1. 什么是缓存Cache

缓存:存储在计算上的一个原始数据复制集

缓存是介于数据访问者与数据源之间的一种高速存储,当数据需要多次读取的时候,用于加快读取的速度。

缓存Cache和缓冲Buffer的区别?

缓存是在读取数据时,把最常用的数据保存在内存的缓存区中,再次读取该数据时,就不再从数据源读取了,而在缓存中读取。

缓冲是向硬盘中写入数据时,先把数据放入缓冲区,然后再一起向硬盘写入,把分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻址,从而提高系统性能。

简单来说,缓存是用来加速数据读取的,而缓冲是用来加速数据写入的。



2. 无处不在的缓存

2.1 CPU缓存

2.2 操作系统缓存

2.3 JVM编译缓存

2.4 CDN缓存

2.5 代理与反向代理缓存

2.6 应用程序缓存

2.7 分布式对象缓存

3. 缓存数据存储(Hash表)

缓存数据存储结构



4. 缓存的关键指标

4.1 缓存命中率

缓存是否有效依赖于能多少次重用同一个缓存响应业务请求,这个度量指标被称作为缓存命中率。如果查询一个缓存,十次查询九次能够得到正确结果,那么它的命中率就是90%。

5. 影响缓存的关键指标

5.1 缓存键集合大小

缓存中的每个对象使用缓存键进行识别,定位一个对象的唯一方式就是对缓存键进行精确匹配。缓存键空间大小是你的应用能够生成的所有键的数量。从统计数字上看,应用生成的唯一键越多,重用的机会越小。一定要想办法减少可能的缓存键数量。键数量越少,缓存的效率越高。

5.2 缓存可使用内存空间

缓存可使用内存空间直接决定了缓存对象的平均大小和缓存对象数量。因为缓存通常存在内存中,缓存对象可用空间收到严格限制且相对昂贵。如果想缓存新的对象,就需要先删除老的对象,再添加新的对象。替换(清除)对象会降低缓存命中率,因为缓存对象删除后,将来的请求就无法命中了,物理上能缓存的对象越多,缓存命中率就越高。

5.3 缓存对象生存时间

缓存对象的生存时间称为TTL。对象缓存的时间越长,缓存对象被重用的可能性就越高。

5.4 缓存分类

5.4.1 通读缓存

代理缓存、反向代理缓存,CDN缓存都是通读缓存。

通读缓存给客户端返回缓存资源,并在请求未命中缓存时获取实际数据。

客户端连接的是通读缓存而不是生成响应的原始服务器。

5.4.2 旁路缓存

对象缓存是一种旁路缓存,旁边缓存通常是一个独立的键值对存储。

应用代码通常会询问对象缓存需要的对象是否存在,如果存在,它会获取并使用缓存的的对象,如果不存在或已过期,应用该会连接主数据源来组装对象,并将其保存回对象缓存中以便将来使用。

5.4.3 浏览器缓存

localStorage

5.5 缓存为什么能提高性能

  • 缓存数据通常来自于内存,比磁盘上的数据有更快的访问速度。

  • 缓存存储数据的最终结果形态,不需要中间计算,减少CPU资源的消耗。

  • 缓存降低数据库、磁盘、网络的负载压力,使这些I/O设备获得更好的响应特性。

5.6 缓存是系统性能优化的大杀器

  • 技术简单

  • 性能提升显著

  • 应用场景多

5.7 合理使用缓存

使用缓存对提高系统性能有很多好处,但是不合理的使用缓存可能非但不能提高系统的性能,还会成为系统的累赘,甚至风险。实践中,缓存滥用的情景屡见不鲜——过分依赖缓存、不合适的数据访问特性等。

5.7.1 频繁修改的数据

这种数据如果缓存起来,由于频繁修改,应用还来不及读取就已失效或更新,徒增系统负担,一般说来,数据的读写比在2:1以上,缓存才有意义。

5.7.2 没有热点的访问

缓存使用过内存作为存储,内存资源宝贵而有限,不能将所有数据都缓存起来,如果应用系统访问数据没有热点,不遵循二八定律,即大部分数据访问不是集中在小部分数据上,那么缓存就没有意义,因为大部分还没有被再次访问就已经被挤出缓存了。

5.7.3 数据不一致与脏读

一般会对缓存的数据设置失效时间,一旦超过失效时间,就要从数据库中重新加载,因此应用要容忍一定时间的数据的不一致。在互联网应用中,这种延迟通常是可以接受的,但是具体应用需要仍需慎重对待。还有一种策略是数据更新时立即更新缓存,不过也会带来更多系统开销和事务一致性的问题。因此数据更新时通知缓存失效,删除该缓存数据,是一种更加稳妥的做法。

5.7.4 缓存穿透

缓存穿透,是指查询一个一定不存在的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从DB查询不到数据则不写入缓存,这将导致这个不存在的数据每次都要到DB去查询,失去了缓存的意义。

被动写:当从缓存中查不到数据时,然后让数据库查询到该数据,写入该数据到缓存中。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。



有两种方案可以解决:

  1. 方案一,缓存空对象。当从DB查询数据为控股,我们仍然对这个空结果进行缓存,具体的值需要使用过特殊的标识,能和真正缓存的数据区分开。另外,需要设置较短的过期时间,一般建议不超过5分钟。

  2. 方案二,BloomFilter 布隆过滤器

在缓存服务的基础上,构建BloomFilter数据结构,在BloomFilter中存储对应的key是否存在,如果存在,说明该KEY对应的值不为空。那么整个逻辑如下:

  • 根据key查询【BloomFilter缓存】。如果不存在对应的值,直接返回;如果存在,继续向下执行。

  • 根据key查询在【数据缓存】中的值。如果存在值,直接返回。如果不存在,继续向下执行。

  • 查询DB对应的值,如果存在,则更新到缓存,并返回该值。

5.7.5 缓存雪崩

缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除时,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。



缓存雪崩的常见解决方案有两种:更新锁机制和后台更新机制。

  • 更新锁

对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或默认值。分布式集群的业务系统要完美实现更新锁机制,需要用到分布式锁技术。

  • 后台更新

由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身设置的有效期为永久,后台线程定时更新缓存。

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

  • 设置热点数据永远不过期。

  • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。

5.7.6 缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。



解决方案

  • 设置热点数据永远不过期

  • 互斥锁

如果缓存中没有数据,第一个进入的线程,获得锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新取缓存中取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现。



二、消息队列

1 什么是消息队列

消息队列,是分布式系统中重要的组件。

  • 主要解决应用耦合,异步消息,流量削峰等问题。

  • 可实现高性能、高可用,可伸缩性和最终一致性架构,是大型分布式系统不可缺少的中间件。

2 消息队列由哪些角色组成

  • 生产者(Producer):负责产生消息。

  • 消费者(Consumer):负责消费消息

  • 消息代理(Message Broker):负责存储消息和转发消息两件事情。其中,转发消息分为推送和拉取两种方式。拉取(Pull),是指 Consumer 主动从 Message Broker 获取消息推送(Push),是指 Message Broker 主动将 Consumer 感兴趣的消息推送给 Consumer 。

3 消息队列有哪些使用场景

一般来说,有四大类使用场景:

  • 应用解耦

  • 异步处理

  • 流量削峰

  • 消息通讯

  • 日志处理

4 消息队列有什么缺点

  • 系统可用性降低。

系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,本来 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?所以,消息队列一定要做好高可用

  • 系统复杂度提高。

主要需要多考虑,1)消息怎么不重复消息。2)消息怎么保证不丢失。3)需要消息顺序的业务场景,怎么处理。

  • 一致性问题。

A 系统处理完了直接返回成功了,人都以为你这个请求就成功了。但是问题是,要是 B、C。D 三个系统那里,B、D 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

当然,这不仅仅是 MQ 的问题,引入 RPC 之后本身就存在这样的问题。如果我们在使用 MQ 时,一定要达到数据的最终一致性。即,C 系统最终执行完成。

5 消息队列有几种消费语义

一共有 3 种,分别如下:

  • 消息至多被消费一次(At most once):消息可能会丢失,但绝不重传。

  • 消息至少被消费一次(At least once):消息可以重传,但绝不丢失。

  • 消息仅被消费一次(Exactly once):每一条消息只被传递一次。



6 消息队列有几种投递方式?分别有什么优缺点

  • push优点,就是及时性。缺点,就是受限于消费者的消费能力,可能造成消息的堆积,Broker 会不断给消费者发送不能处理的消息。

  • pull优点,就是主动权掌握在消费方,可以根据自己的消息速度进行消息拉取。缺点,就是消费方不知道什么时候可以获取的最新的消息,会有消息延迟和忙等。



目前的消息队列,基于 push + pull 模式结合的方式,Broker 仅仅告诉 Consumer 有新的消息,具体 的消息拉取,还是 Consumer 自己主动拉取。



7 如何保证服务器的消费消息的幂等性

消费者实现幂等性,有两种方式:



  1. 框架层统一封装。

  2. 业务层自己实现。



① 框架层统一封装



首先,需要有一个消息排重的唯一标识,该编号只能由 Producer 生成,例如说使用 uuid、或者其它唯一编号的算法 。



然后,就需要有一个排重的存储器,例如说:



  • 使用关系数据库,增加一个排重表,使用消息编号作为唯一主键。

  • 使用 KV 数据库,KEY 存储消息编号,VALUE 任一。此处,暂时不考虑 KV 数据库持久化的问题



那么,我们要什么时候插入这条排重记录呢?



  • 在消息消费执行业务逻辑之前,插入这条排重记录。但是,此时会有可能 JVM 异常崩溃。那么 JVM 重启后,这条消息就无法被消费了。因为,已经存在这条排重记录。

  • 在消息消费执行业务逻辑之后,插入这条排重记录。如果业务逻辑执行失败,显然,我们不能插入这条排重记录,因为我们后续要消费重试。如果业务逻辑执行成功,此时,我们可以插入这条排重记录。但是,万一插入这条排重记录失败呢?那么,需要让插入记录和业务逻辑在同一个事务当中,此时,我们只能使用数据库



② 业务层自己实现



方式很多,这个和 HTTP 请求实现幂等是一样的逻辑:



  • 先查询数据库,判断数据是否已经被更新过。如果是,则直接返回消费完成,否则执行消费。

  • 更新数据库时,带上数据的状态。如果更新失败,则直接返回消费完成,否则执行消费。



如果胖友的系统的并发量非常大,可以使用 Zookeeper 或者 Redis 实现分布式锁,避免并发带来的问题。当然,引入一个组件,也会带来另外的复杂性:



  1. 系统的并发能力下降。

  2. Zookeeper 和 Redis 在获取分布式锁时,发现它们已经挂掉,此时到底要不要继续执行下去呢?嘿嘿。



选择



正常情况下,出现重复消息的概率其实很小,如果由框架层统一封装来实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务层自己实现处理消息重复的问题。



当然,这两种方式不是冲突的。可以提供不同类型的消息,根据配置,使用哪种方式。例如说:



  • 默认情况下,开启【框架层统一封装】的功能。

  • 可以通过配置,关闭【框架层统一封装】的功能。

8 如何解决消息积压的问题

发布于: 2020 年 07 月 06 日 阅读数: 57
用户头像

王海

关注

还未添加个人签名 2018.06.17 加入

还未添加个人简介

评论

发布
暂无评论
架构师训练营学习总结——缓存与消息队列【第五周】