JAVA并发编程与高并发解决方案 - 视频第13章(高并发之消息队列思路)

还没有看这篇文章~消息队列使用的四种场景介绍

参考
https://suprisemf.github.io/2018/08/03/消息队列/
https://blog.csdn.net/lengyuezuixue/article/details/81232193

1、消息队列

消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合可靠投递广播流量控制最终一致性等一系列功能,成为异步RPC的主要手段之一
JAVA并发编程与高并发解决方案 - 视频第13章(高并发之消息队列思路)_第1张图片
消息被处理的过程相当于流程A被处理。我们这里以一个实际的模型来讨论下,比如用户下单成功时给用户发短信,如果没有这个消息队列,我们会选择同步调用发短信的接口,并等待短息发送成功,这时候假设短信接口实现出现问题了,或者短信调用端超时了,又或者短信发送达到上限了,我们是选择重试几次还是放弃,还是选择把这个放到数据库过一段时间再看看呢,不管怎样,实现都很复杂。

我们可以将发短信这个请求放在消息队列里,消息队列按照一定的顺序处理队列里的消息,当处理到发送短信的任务时,通知短信服务发送消息,如果出现之前出现的问题,那么把这个消息重新放到消息队列中。

1.1消息队列的特性

  • 业务无关,一个具有普适性质的消息队列组件不需要考虑上层的业务模型,只做好消息的分发就可以了,上层业务的不同模块反而需要依赖消息队列所定义的规范进行通信。

  • FIFO,先投递先到达的保证是一个消息队列和一个buffer的本质区别。

  • 容灾,对于普适的消息队列组件来说,节点的动态增删消息的持久化,都是支持其容灾能力的重要基本特性。当然,这个特性对于游戏服务器中大部分应用中的消息队列来说不是必须的,这个也是跟应用情景有关的,很多时候没有这种持久化的需求。

  • 性能,这个不必多说了,消息队列的吞吐量上去了,整个系统的内部通信效率也会有提高。

为什么需要消息队列

当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消息队列,作为抽象层,弥合双方的差异。“ 消息 ”是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。消息被发送到队列中,“ 消息队列 ”是在消息的传输过程中保存消息的容器 。

举几个例子

1)业务系统触发短信发送申请,但短信发送模块速度跟不上,需要将来不及处理的消息暂存一下,缓冲压力。就可以把短信发送申请丢到消息队列直接返回用户成功,短信发送模块再可以慢慢去消息队列中取消息进行处理

2)调远程系统下订单成本较高,且因为网络等因素,不稳定,攒一批一起发送。

3)任务处理类的系统,先把用户发起的任务请求接收过来存到消息队列中,然后后端开启多个应用程序从队列中取任务进行处理。

1.2消息队列的好处

1、成功完成了一个异步解耦的过程。短信发送时只要保证放到消息队列中就可以了,接着做后面的事情就行。一个事务只关心本质的流程,需要依赖其他事情但是不那么重要的时候,有通知即可,无需等待结果。每个成员不必受其他成员影响,可以更独立自主,只通过一个简单的容器来联系。

对于我们的订单系统,订单最终支付成功之后可能需要给用户发送短信积分什么的,但其实这已经不是我们系统的核心流程了。如果外部系统速度偏慢(比如短信网关速度不好),那么主流程的时间会加长很多,用户肯定不希望点击支付过好几分钟才看到结果。那么我们只需要通知短信系统“我们支付成功了”,不一定非要等待它处理完成。

2、保证了最终一致性,通过在队列中存放任务保证它最终一定会执行。

最终一致性指的是两个系统的状态保持一致,要么都成功,要么都失败。当然有个时间限制,理论上越快越好,但实际上在各种异常的情况下,可能会有一定延迟达到最终一致状态,但最后两个系统的状态是一样的。
业界有一些为“最终一致性”而生的消息队列,如Notify(阿里)、QMQ(去哪儿)等,其设计初衷,就是为了交易系统中的高可靠通知。
以一个银行的转账过程来理解最终一致性,转账的需求很简单,如果A系统扣钱成功,则B系统加钱一定成功。反之则一起回滚,像什么都没发生一样。
然而,这个过程中存在很多可能的意外:

A扣钱成功,调用B加钱接口失败。
A扣钱成功,调用B加钱接口虽然成功,但获取最终结果时网络异常引起超时。
A扣钱成功,B加钱失败,A想回滚扣的钱,但A机器down机。

可见,想把这件看似简单的事真正做成,真的不那么容易。所有跨VM的一致性问题,从技术的角度讲通用的解决方案是:

强一致性,分布式事务,但落地太难且成本太高,后文会具体提到。
最终一致性,主要是用“记录”和“补偿”的方式。在做所有的不确定的事情之前,先把事情记录下来,然后去做不确定的事情,结果可能是:成功、失败或是不确定,“不确定”(例如超时等)可以等价为失败。成功就可以把记录的东西清理掉了,对于失败和不确定,可以依靠定时任务等方式把所有失败的事情重新搞一遍,直到成功为止。
回到刚才的例子,系统在A扣钱成功的情况下,把要给B“通知”这件事记录在库里(为了保证最高的可靠性可以把通知B系统加钱和扣钱成功这两件事维护在一个本地事务里),通知成功则删除这条记录,通知失败或不确定则依靠定时任务补偿性地通知我们,直到我们把状态更新成正确的为止。

3、广播

消息队列的基本功能之一是进行广播。如果没有消息队列,每当一个新的业务方接入,我们都要联调一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。

4、削峰和流控。

不对于不需要实时处理的请求来说,当并发量特别大的时候,可以先在消息队列中作缓存,然后陆续发送给对应的服务去处理。

试想上下游对于事情的处理能力是不同的。比如,Web前端每秒承受上千万的请求,并不是什么神奇的事情,只需要加多一点机器,再搭建一些LVS负载均衡设备和Nginx等即可。但数据库的处理能力却十分有限,即使使用SSD加分库分表,单机的处理能力仍然在万级。由于成本的考虑,我们不能奢求数据库的机器数量追上前端。

这种问题同样存在于系统和系统之间,如短信系统可能由于短板效应,速度卡在网关上(每秒几百次请求),跟前端的并发量不是一个数量级。但用户晚上个半分钟左右收到短信,一般是不会有太大问题的。如果没有消息队列,两个系统之间通过协商、滑动窗口等复杂的方案也不是说不能实现。但系统复杂性指数级增长,势必在上游或者下游做存储,并且要处理定时、拥塞等一系列问题。而且每当有处理能力有差距的时候,都需要单独开发一套逻辑来维护这套逻辑。所以,利用中间系统转储两个系统的通信内容,并在下游系统有能力处理这些消息的时候,再处理这些消息,是一套相对较通用的方式。

总而言之,消息队列不是万能的。对于需要强事务保证而且延迟敏感的,RPC是优于消息队列的

对于一些无关痛痒,或者对于别人非常重要但是对于自己不是那么关心的事情,可以利用消息队列去做。

支持最终一致性的消息队列,能够用来处理延迟不那么敏感的“分布式事务”场景,而且相对于笨重的分布式事务,可能是更优的处理方式。

当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的“漏斗”。在下游有能力处理的时候,再进行分发。

如果下游有很多系统关心你的系统发出的通知的时候,果断地使用消息队列吧。

2、初识Kafka

Kafka是Apache基金会下的一个开源项目。是一个高性能跨语言的分布式发布订阅(PUB/SUB)的消息队列系统

其处理流程图为;

JAVA并发编程与高并发解决方案 - 视频第13章(高并发之消息队列思路)_第2张图片

2.1Kafka特性

1、可快速持久化。通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能。

2、高吞吐量。即使是非常普通的硬件Kafka也可以支持每秒数百万的消息。

3、完全的分布式系统。它的Broker、Producer、Consumer都原生地支持分布式,自动支持负载均衡。

4、支持Hadoop的数据并行加载。可通过hadoop的并行加载机制统一处理在线、离线的消息。

5、支持通过Kafka服务器和消费机集群来分区消息。

2.2Kafka相关术语

对应于上图:

  • Broker
    Kafka集群包含一个或多个服务器,这种服务器被称为broker。
  • Topic
    每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
  • Partition
    Partition是物理上的概念,每个Topic包含一个或多个Partition.
  • Producer
    负责发布消息到Kafka broker
  • Consumer
    消息消费者,向Kafka broker读取消息的客户端。
  • Consumer Group
    每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。

3、初识RabbitMQ

相对于其他消息队列,RabbitMQ有自己的服务器的管理界面。

处理流程图如下:

JAVA并发编程与高并发解决方案 - 视频第13章(高并发之消息队列思路)_第3张图片

其中的概念说明:

  • Broker:简单来说就是消息队列服务器实体。
  • Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
  • Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
    Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
    Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
  • vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
  • producer:消息生产者,就是投递消息的程序。
  • consumer:消息消费者,就是接受消息的程序。
  • channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
  • ExchangeType(关键):交换策略。指定exchange分发消息到哪一个或多个队列中。其有四种类型:Direct , Fanout , Topic , Handers

其处理流程为

  1. 客户端连接到消息队列服务器,打开一个channel。
  2. 客户端声明一个exchange,并设置相关属性。
  3. 客户端声明一个queue,并设置相关属性。
  4. 客户端使用RoutingKey,在exchange和queue之间建立好绑定关系。
  5. 客户端投递消息到exchange。
  6. exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里。
  7. 客户端只需要负责处理消息。

你可能感兴趣的:(线程,并发,定时任务)