MQ的三大特点:削峰、异步、解耦
1.RabblitMQ概念介绍
1.1概念
RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
1.2基本结构
1.2.1服务主机Broker
一个搭建RabbitMQ Server的服务器称为Broker。这个并不是RabbitMQ特有的概念,但是却是几乎所有MQ产品通用的一个概念。未来如果需要搭建集群,就需要通过这些Broker来构建。
1.2.2虚拟主机virtual host
RabbitMQ出于服务器复用的想法,可以在一个RabbitMQ集群中划分出多个虚拟主机,每一个虚拟主机都有全套的基础服务组件,可以针对每个虚拟主机进行权限以及数据分配。不同虚拟主机之间是完全隔离的,如果不考虑资源分配的情况,一个虚拟主机就可以当成一个独立的RabbitMQ服务使用。
1.2.3连接Connection
客户端与RabbitMQ进行交互,首先就需要建立一个TCP连接,这个连接就是Connection。既然是通道,那就需要尽量注意在停止使用时要关闭,释放资源。
1.2.4信道Channel
一旦客户端与RabbitMQ建立了连接,就会分配一个AMQP信道Channel。每个信道都会被分配一个唯一的ID。也可以理解为是客户端与RabbitMQ实际进行数据交互的通道,我们后续的大多数的数据操作都是在信道Channel这个层面展开的。
RabbitMQ为了减少性能开销,也会在一个Connection中建立多个Channel,这样便于客户端进行多线程连接,这些连接会复用同一个Connection的TCP通道,所以在实际业务中,对于Connection和Channel的分配也需要根据实际情况进行考量。
1.2.5交换机Exchange
这是RabbitMQ中进行数据路由的重要组件。消息发送到RabbitMQ中后,会首先进入一个交换机,然后由交换机负责将数据转发到不同的队列中。RabbitMQ中有多种不同类型的交换机来支持不同的路由策略。从 Web管理界面就能看到,在每个虚拟主机中,RabbitMQ都会默认创建几个不同类型的交换机来。
1.2.6队列Queue
Queue是实际保存数据的最小单位。Queue不需要Exchange也可以独立工作,只不过通常在业务场景中,会增加Exchange实现更复杂的消息分配策略。Queue结构天生就具有FIFO的顺序,消息最终都会被分发到不同的Queue当中,然后才被消费者进行消费处理。这也是最近RabbitMQ功能变动最大的地方。最为常用的是经典队列Classic。RabbitMQ 3.8.X版本添加了Quorum队列,3.9.X又添加了Stream队列。从官网的封面就能看到,现在RabbitMQ主推的是Quorum队列。
1.3工作原理
黄色的圈圈就是我们的消息推送服务,将消息推送到中间方框里面也就是rabbitMq的服务器,然后经过服务器里面的交换机、队列等各种关系将数据处理入列后,最终右边的蓝色圈圈消费者获取对应监听的消息。
生产者发送消息流程:
1、生产者和Broker建立TCP连接。
2、生产者和Broker建立通道。
3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
4、Exchange将消息转发到指定的Queue(队列)
消费者接收消息流程:
1、消费者和Broker建立TCP连接
2、消费者和Broker建立通道
3、消费者监听指定的Queue(队列)
4、当有消息到达Queue时Broker默认将消息推送给消费者。
5、消费者接收到消息。
6、ack回复
1.4RabblitMQ交换机类型
1.4.1Direct exchange(直连交换机)
直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应队列的,步骤如下:
1、将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键(routing key)
2、当一个携带着路由值为R的消息被发送给直连交换机时,交换机会把它路由给绑定值同样为R的队列。
1.4.2Fanout exchange(扇形交换机)
扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列。
1.4.3Topic exchange(主题交换机)
主题交换机(topic exchanges)中,队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。
扇型交换机和主题交换机异同:
- 对于扇型交换机路由键是没有意义的,只要有消息,它都发送到它绑定的所有队列上
- 对于主题交换机,路由规则由路由键决定,只有满足路由键的规则,消息才可以路由到对应的队列上
介绍一下规则
* (星号) 用来表示一个单词 (必须出现的)
#(井号) 用来表示任意数量(零个或多个)单词
通配的绑定键是跟队列进行绑定的,举个小例子:
队列Q1 绑定键为*.TT.* 队列Q2绑定键为 TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;
1.4.4Headers Exchange
Headers类型的Exchange就是一种忽略routingKey的路由方式。他通过Headers来进行消息路由。
这个headers是一个键值对,发送者可以在发送的时候定义一些键值对,接受者也可以在绑定时定义自己的键值对。当键值对匹配时,对应的消费者就能接收到消息。匹配的方式有两种,一种是all,表示需要所有的键值对都满足才行。另一种是any,表示只要满足其中一个键值就可以了。
2.RabbitMQ的常用消息场景
主要是对Exchange进行深度使用。
2.1Hello World
最直接的方式,P端发送一个消息到一个指定的queue,中间不需要任何exchange规则。C端按queue方式进行消费。
2.2Work Queues工作序列
这是RabbitMQ最基础也是最常用的一种工作机制。
Producer消息发送给queue,多个Consumer同时往队列上消费消息。
重点讲解如下:
首先。Consumer端的autoAck字段设置的是false,这表示consumer在接收到消息后不会自动反馈服务器已消费了message,而要改在对message处理完成了之后,再调用channel.basicAck来通知服务器已经消费了该message。这样即使Consumer在执行message过程中出问题了,也不会造成message被忽略,因为没有ack的message会被服务器重新进行投递。
但是,这其中也要注意一个很常见的BUG,就是如果所有的consumer都忘记调用basicAck()了,就会造成message被不停的分发,也就造成不断的消耗系统资源。这也就是 Poison Message(毒消息)
其次,官方特意提到的message的持久性。关键的message不能因为服务出现问题而被忽略。还要注意,官方特意提到,所有的queue是不能被多次定义的。如果一个queue在开始时被声明为durable,那在后面再次声明这个queue时,即使声明为 not durable,那这个queue的结果也还是durable的。
然后,是中间件最为关键的分发方式。这里,RabbitMQ默认是采用的fair dispatch,也叫round-robin模式,就是把消息轮询,在所有consumer中轮流发送。这种方式,没有考虑消息处理的复杂度以及consumer的处理能力。而他们改进后的方案,是consumer可以向服务器声明一个prefetchCount,我把他叫做预处理能力值。channel.basicQos(prefetchCount);表示当前这个consumer可以同时处理几个message。这样服务器在进行消息发送前,会检查这个consumer当前正在处理中的message(message已经发送,但是未收到consumer的basicAck)有几个,如果超过了这个consumer节点的能力值,就不再往这个consumer发布。
这种模式,官方也指出还是有问题的,消息有可能全部阻塞,所有consumer节点都超过了能力值,那消息就阻塞在服务器上,这时需要自己及时发现这个问题,采取措施,比如增加consumer节点或者其他策略。
2.3Publish/Subscribe订阅/发布机制
这个机制是对上面的一种补充。也就是把preducer与Consumer进行进一步的解耦。producer只负责发送消息,至于消息进入哪个queue,由exchange来分配。如上图,就是把producer发送的消息,交由exchange同时发送到两个queue里,然后由不同的Consumer去进行消费。
2.4Routing基于内容的路由
在上一章 exchange 往所有队列发送消息的基础上,增加一个路由配置,指定 exchange如何将不同类别的消息分发到不同的queue上。
2.5Topics基于话题的路由
这个模式也就在上一个模式的基础上,对routingKey进行了模糊匹配。
单词之间用,隔开,* 代表一个具体的单词。# 代表0个或多个单词。
2.6Publisher Confirms发送者消息确认
而这个模块就是通过给发送者提供一些确认机制,来保证这个消息发送的过程是成功的。
发送者确认模式默认是不开启的,所以如果需要开启发送者确认模式,需要手动在channel中进行声明。
2.6.1发送单条消息
即发布一条消息就确认一条消息。
这个方法会同步阻塞channel,在等待确认期间,channel将不能再继续发送消息,也就是说会明显降低集群的发送速度即吞吐量。
如果到了超时时间,还没有收到服务端的确认机制,那就会抛出异常。然后通常处理这个异常的方式是记录错误日志或者尝试重发消息,但是尝试重发时一定要注意不要使程序陷入死循环。
2.6.2发送批量消息
发送一批消息后,再一起确认。
缺点:这种方式可以稍微缓解下发送者确认模式对吞吐量的影响。但是也有个固有的问题就是,当确认出现异常时,发送者只能知道是这一批消息出问题了, 而无法确认具体是哪一条消息出了问题。所以接下来就需要增加一个机制能够具体对每一条发送出错的消息进行处理。
2.6.3异步确认消息
Producer在channel中注册监听器来对消息进行确认。
全局递增的序列号(需要客户端自己对应序列号和消息)和一个boolean类型的参数。
实际上,在当前版本中,Publisher不光可以确认消息是否到了Exchange,还可以确认消息是否从Exchange成功路由到了Queue。在Channel中可以添加一个ReturnListener。这个ReturnListener就会监控到这一部分发送成功了,但是无法被Consumer消费到的消息。
2.7Hraders头部路由机制
官网示例中的集中路由策略, direct,fanout,topic等这些Exchange,都是以routingkey为关键字来进行消息路由的,但是这些Exchange有一个普遍的局限就是都是只支持一个字符串的形式,而不支持其他形式。
Headers类型的Exchange就是一种忽略routingKey的路由方式。他通过Headers来进行消息路由。这个 headers是一个键值对,发送者可以在发送的时候定义一些键值对,接受者也可以在绑定时定义自己的键值对。当键值对匹配时,对应的消费者就能接收到消息。匹配的方式有两种,一种是all,表示需要所有的键值对都满足才行。另一种是any,表示只要满足其中一个键值就可以了。
3.Queue 队列
如果消息产生大量积累就会严重影响消息收发的性能。
3.1三种队列
3.1.1Classic经典队列
经典队列可以选择是否持久化(Durability)以及是否自动删除(Auto delete)两个属性。
在RabbitMQ中,经典队列是一种非常传统的队列结构。消息以FIFO先进先出的方式存入队列。消息被 Consumer从队列中取出后就会从队列中删除。如果消息需要重新投递,就需要再次入队。这种队列都依靠各个Broker自己进行管理,在分布式场景下,管理效率是不太高的。并且这种经典队列不适合积累太多的消息。如果队列中积累的消息太多了,会严重影响客户端生产消息以及消费消息的性能。因此,经典队列主要用在数据量比较小,并且生产消息和消费消息的速度比较稳定的业务场景。比如内部系统之间的服务调用。
3.1.2Quorum仲裁队列
仲裁队列,是RabbitMQ从3.8.0版本,引入的一个新的队列类型。
仲裁队列相比Classic经典队列,在分布式环境下对消息的可靠性保障更高。
Quorum是基于Raft一致性协议实现的一种新型的分布式消息队列,他实现了持久化,多备份的FIFO队列,主要就是针对RabbitMQ的镜像模式设计的。简单理解就是quorum队列中的消息需要有集群中多半节点同意确认后,才会写入到队列中。
Quorum队列处理Poison Message handling(处理有毒的消息)。所谓毒消息是指消息一直不能被消费者正 常消费(可能是由于消费者失败或者消费逻辑有问题等),就会导致消息不断的重新入队,这样这些消息就成为了毒消息。这些读消息应该有保障机制进行标记并及时删除。Quorum队列会持续跟踪消息的失败投递尝试次数,并记录在"x-delivery-count"这样一个头部参数中。然后,就可以通过设置 Delivery limit参数来定制一个毒消息的删除策略。当消息的重复投递次数超过了Delivery limit参数阈值时,RabbitMQ就会删除这些毒消息。当然,如果配置了死信队列的话,就会进入对应的死信队列。
3.1.3Stream流式队列
Stream队列是RabbitMQ自3.9.0版本开始引入的一种新的数据队列类型。这种队列类型的消息是持久化到磁盘并且具备分布式备份的,更适合于消费者多,读消息非常频繁的场景。
Stream队列的核心是以append-only只添加的日志来记录消息,整体来说,就是消息将以append-only的方式持久化到日志文件中,然后通过调整每个消费者的消费进度offset,来实现消息的多次分发。
这种队列提供了RabbitMQ已有的其他队列类型不太好实现的四个特点:
1.large fan-outs 大规模分发;
当想要向多个订阅者发送相同的消息时,以往的队列类型必须为每个消费者绑定一个专用的队列。如果消费者的数量很大,这就会导致性能低下。而Stream队列允许任意数量的消费者使用同一个队列的消息,从而消除绑定多个队列的需求。
2.Replay/Time-travelling 消息回溯;
RabbitMQ已有的这些队列类型,在消费者处理完消息后,消息都会从队列中删除,因此,无法重新读取已经消费过的消息。而Stream队列允许用户在日志的任何一个连接点开始重新读取数据。
3.Throughput Performance 高吞吐性能;
Strem队列的设计以性能为主要目标,对消息传递吞吐量的提升非常明显
4.Large logs 大日志
RabbitMQ一直以来有一个让人诟病的地方,就是当队列中积累的消息过多时,性能下降会非常明显。但是Stream队列的设计目标就是以最小的内存开销高效地存储大量的数据。使用Stream队列可以比较轻松的在队列中积累百万级别的消息。
3.2死信队列
它是RabbitMQ对于未能正常消费的消息进行的一种补救机制。死信队列也是一个普通的队列,同样可以在队列上声明消费者,继续对消息进行消费处理。
它需要指定一个交换机作为死信交换机,死信交换机就可以像普通交换机一样,通过RoutingKey将消息转发到对应的死信队列中。
3.2.1产生死信的时机
有以下三种情况,RabbitMQ会将一个正常消息转成死信。
- 消息被消费者确认拒绝。消费者把requeue参数设置为true(false)(重新入队),并且在消费后,向RabbitMQ返回拒绝。channel.basicReject或者channel.basicNack。
- 消息达到预设的TTL时限还一直没有被消费。
- 消息由于队列已经达到最长长度限制而被丢掉。
注意:TTL即最长存活时间Time-To-Live。消息在队列中保存时间超过这个TTL,即会被认为死亡。死亡的消息会被丢入死信队列,如果没有配置死信队列的话,RabbitMQ会保证死了的消息不会再次被投递,并且在未来版本中,会主动删除掉这些死掉的消息。
3.2.2如何确定一个消息是不是死信
消息被作为死信转移到死信队列后,会在Header当中增加一些消息。比如时间、原因(rejected,expired, maxlen)、队列等。然后header中还会加上第一次成为死信的三个属性,并且这三个属性在以后的传递过程中都不会更改。
- x-first-death-reason
- x-first-death-queue
- x-first-death-exchange
3.2.2基于TTL和死信队列实现延迟队列
RabbitMQ中,是不存在延迟队列的功能的,而通常如果要用到延迟队列,就会采用TTL+死信队列的方式来处理。
步骤:生产者首先将消息设置TTL,然后发送到队列A,待消息超过TTL时,将消息传入死信队列B;消费者不设置消费队列A的操作,只设置消费死信队列B的操作。等有消息到达死信队列B时,消费者消费它。
3.3懒队列
RabbitMQ从3.6.0版本开始,就引入了懒队列(Lazy Queue)的概念。
懒队列会尽可能早的将消息内容保存到硬盘当中,并且只有在用户请求到时,才临时从硬盘加载到RAM内存当中。
懒队列会尝试尽可能早的把消息写到硬盘中。这意味着在正常操作的大多数情况下,RAM中要保存的消息要少得多。当然,这是以增加磁盘IO为代价的。消息写入 硬盘的过程中,是会阻塞队列的。
懒队列适合 消息量大 且 长期有堆积 的队列,可以减少内存使用,加快消费速度。但是这是以大量消耗集群的网络及磁盘IO为代价的。
4.插件
4.1Sharding插件
对于RabbitMQ同样,针对单个队列,如何增加吞吐量呢? 消费者并不能对消息增加消费并发度,所以,RabbitMQ的集群机制并不能增加单个队列的吞吐量。
上面的懒队列其实就是针对这个问题的一种解决方案。但是很显然,懒队列的方式属于治标不治本。真正要提升RabbitMQ单队列的吞吐量,还是要从数据也就是消息入手,只有将数据真正的分开存储才行。RabbitMQ提供的Sharding插件,就是一个可选的方案。他会真正将一个队列中的消息分散存储到不同的节点上,并提供多个节点的负载均衡策略实现对等的读与写功能。
注意:
使用Sharding插件后,Producer发送消息时,只需要指定虚拟Exchange,并不能确定消息最终会发往哪一个分片队列。而Sharding插件在进行消息分散存储时,虽然尽量是按照轮询的方式,均匀的保存消息。但是,这并不能保证消息就一定是均匀的。
首先,这些消息在分片的过程中,是没有考虑消息顺序的,这会让RabbitMQ中原本就不是很严谨的消息顺序变得更加雪上加霜。所以,Sharding插件适合于那些对于消息延迟要求不严格,以及对消费顺序没有任何要求的的场景。
然后,Sharding插件消费伪队列的消息时,会从消费者最少的碎片中选择队列。这时,如果你的这些碎片队列中已经有了很多其他的消息,那么再去消费伪队列消息时,就会受到这些不均匀数据的影响。所以,如果使用Sharding插件,这些碎片队列就尽量不要单独使用了(不要单独指定消费者)。(数据分片后,还是希望能够像一个普通队列一样消费到完整的数据副本。这时,Sharding插件提供了一种伪队列的消费方式。)