官网地址:RabbitMQ: easy to use, flexible messaging and streaming — RabbitMQhttps://www.rabbitmq.com/
消息队列(Message Queue,简称MQ),从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已。
其主要用途:不同进程Process/线程Thread之间通信。
目前市场上主流的MQ有三款:
在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务,用于上下游传递消息。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。
常见的MQ消息中间件有很多,例如ActiveMQ
、RabbitMQ
、Kafka
、RocketMQ
等等。那么为什么我们要使用它呢?因为它能很好的帮我们解决一些复杂特殊的场景:
假设某订单系统每秒最多能处理一万次订单,也就是最多承受的10000qps,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息队列,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。
以订单服务为例,传统的方式为单体应用,支付、修改订单状态、创建物流订单三个步骤集成在一个服务中,因此这三个步骤可以放在一个jdbc事务中,要么全成功,要么全失败。而在微服务的环境下,会将三个步骤拆分成三个服务,例如:支付服务,订单服务,物流服务。三者各司其职,相互之间进行服务间调用,但这会带来分布式事务的问题,因为三个步骤操作的不是同一个数据库,导致无法使用jdbc事务管理以达到一致性。而 MQ 能够很好的帮我们解决分布式事务的问题,有一个比较容易理解的方案,就是二次提交。基于MQ的特点,MQ作为二次提交的中间节点,负责存储请求数据,在失败的情况可以进行多次尝试,或者基于MQ中的队列数据进行回滚操作,是一个既能保证性能,又能保证业务一致性的方案。
MQ 具有发布订阅机制,不仅仅是简单的上游和下游一对一的关系,还有支持一对多或者广播的模式,并且都可以根据规则选择分发的对象。这样一份上游数据,众多下游系统中,可以根据规则选择是否接收这些数据,能达到很高的拓展性。
虽然市面上的MQ数量众多、种类繁杂,但MQ其本质上就是用来暂时存放消息的一种中间件,其实从三个角度去关注MQ即可抓住MQ的核心:
消息可靠性,即消息会不会丢失?围绕防止消息丢失做了哪些工作?
消息模型,即支持以什么样的模式去消费消息?点对点?广播?发布订阅?其消息模型丰富度如何?
MQ作为用来减轻系统压力的中间件,其自身势必会经常面对很大的流量,吞吐量如何自然是要考虑的。
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
RabbitMQ是一套开源(MPL)的消息队列服务软件,是由 LShift 提供的一个 Advanced Message Queuing Protocol (AMQP) 的开源实现,由以高性能、健壮以及可伸缩性出名的Erlang写成。
支持的操作系统:
支持的编程语言:
Erlang——面向并发的编程语言
Erlang是一种通用的面向并发的编程语言,它由瑞典电信设备提供商爱立信所辖的CS-Lab开发,目的是创造一种可以应对大规模并发活动的编程语言和运行环境。Erlang问世于1987年,经过十年的发展,于1998年发布开源版本。Erlang是运行于虚拟机的解释性语言,但是也包含有乌普萨拉大学高性能Erlang计划(HiPE)开发的本地代码编译器,自R11B-4版本开始,Erlang也开始支持脚本式解释器。在编程范型上,Erlang属于多重范型编程语言,涵盖函数式、并发式及分布式。顺序执行的Erlang是一个及早求值,单次赋值和动态类型的函数式编程语言。
Erlang是一个结构化,动态类型编程语言,内建并行计算支持。最初是由爱立信专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适 合于构建分布式,实时软并行计算系统。使用Erlang编写出的应用运行时通常由成千上万个轻量级进程组成,并通过消息传递相互通讯。进程间上下文切换对于Erlang来说仅仅 只是一两个环节,比起C程序的线程切换要高效得多得多了。
使用Erlang来编写分布式应用要简单的多,因为它的分布式机制是透明的:对于程序来说并不知道自己是在分布式运行。Erlang运行时环境是一个虚拟机,有点像Java虚拟机,这样代码一经编译,同样可以随处运行。它的运行时系统甚至允许代码在不被中断 的情况下更新。另外如果需要更高效的话,字节代码也可以编译成本地代码运行。
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
AMQP所覆盖的内容包含了网络协议以及服务端服务
AMQP像是一个把东西连在一起的语言,而不是一个系统。其设计目标是:让服务端可通过协议编程。
AMQP协议是一个二进制协议,具有一些现代特性:多通道(multi-channel),可协商(negotiated),异步、安全、便携、语言中立、高效的。其协议主要分成两层:
这样分层之后,可以把传输层替换为其它传输协议,而不需要修改功能层。同样,也可以使用同样的传输层,基于此实现不同的上层协议。可能RabbitMQ也是因为类似的原因,能够比较容易的支持MQTT、STOMP等协议的吧。
主要包含了三个主要组件
可以把AMQP的架构理解为一个邮件服务:
Cc:
, Bcc:
的地址。不包含服务端信息。在AMQP里,生产者直接把消息发到服务端,服务端再把这些消息路由到邮箱中。消费者直接从邮箱里取消息。但在AMQP之前的很多中间件中,发布者是把消息直接发到对应的邮箱里(类似于存储发布队列),或者直接发到邮件列表里(类似topic订阅)。
这里的主要区别在于,用户可以控制消息队列和交换器的绑定规则,而不是依赖中间件自身的代码。这样就可以做很多有趣的事情。比如定义一个这样的规则:把所有包含这样和这样Header的消息,都复制一份到这个消息队列中。“
在这个过程中,生产者只能把所有消息发到一个单点(交换器),而不能直接把消息发到某个消息队列(message-queue)中。
每个AMQP服务端都会自己创建一些交换器,这些不能被销毁。AMQP程序也可以创建其自己的交换器。AMQP并不使用create这个方法,而是使用declare方法来表示:如果不存在,则创建,存在了则继续。程序可以创建交换器用于私有使用,并在任务完成后销毁它们。虽然AMQP提供了销毁交换器的方法,但一般来讲程序不需要销户它。
队列分两种
绑定是交换器和消息队列之间的关系,告诉交换器如何路有消息。
//绑定命令的伪代码
Queue.Bind To WHERE
Queue.Declare queue=app.svc01 // 声明一个叫做 app.svc01 的队列
// Comsumer
Basic.Consume queue=app.svc01 // 消费者消费该队列
// Producer
Basic.Publish routing-key=app.svc01 // 生产者发布消息。routingKey为队列名称
一般来讲,回复队列是私有的、临时的、由服务端命名、只有一个消费者。(没有直接使用AMQP协议中的例子,而是使用了RabbitMQ的例子)
Queue.Declare queue=rpc_queue // 调用的队列
// Server
Basic.Consume queue=rpc_queue
// Client
Queue.Declare queue= exclusive=TRUE
S:Queue.Declare-Ok queue=amq.gen-X... // AMQP服务端告诉队列名称
Basic.Publish queue=rpc_queue reply_to=amq_gen-X... // 客户端向服务端发送请求
// Server
handleMessage()
// 服务端处理好消息后,向消息列的reply-to字段中的队列发送响应
Basic.Publish exchange= routing-key={message.replay_to}
在传统的中间件中,术语subscription含糊不清。至少包含两个概念:匹配消息的条件集,和一个临时队列用于存放匹配的消息。AMQP把这两部分拆成:binding和message queue。在AMQP中,并没有一个实体叫做subscription。
AMQP的发布订阅模型为:
订阅队列与命名队列或回复队列之间的关键区别在于,订阅队列名称与路由目的无关,并且路由是根据抽象的匹配条件完成的,而不是路由键字段的一对一匹配。
// Consumer
Queue.Declare queue= exclusive=TRUE
// 这里是使用服务端下发的队列名称,并设置为独占。
// 也可以使用约定的队列名称。这样就相当于把发布-订阅模型与共享队列组合使用了
S:Queue.Declare-Ok queue=tmp.2
Queue.Bind queue=tmp.2 TO exchange=amq.topic WHERE routing-key=*.orange.*
Basic.Consume queue=tmp.2
// Producer
Basic.Publish exchange=amq.topic routing-key=quick.orange.rabbit
中间件复杂度很高,所以设计协议时的挑战是要驯服其复杂性。AMQP采用方法是基于类来建立传统API模型。类中包含方法,并定义了方法明确应该做什么。
AMQP中有两种不同的方式进行对话:
AMQP是一个长连接协议。Connection被设计为长期使用的,可以携带多个Channel。Connection的生命周期是:
如果在发送或者收到 Open 或者 Open-Ok 之前,某一个节点发现了一个错误,则必须直接关闭Socket,且不发送任何数据。
AMQP是一个多通道协议。Channel提供了一种方式,在比较重的TCP/IP连接上建立多个轻量级的连接。这会让协议对防火墙更加友好,因为端口使用是可预知的。它也意味着很容易支持流量调整和其他QoS特性。
Channels相互是独立的,可以同步执行不同的功能。可用带宽会在当前活动之间共享。
这里期望也鼓励多线程客户端程序应该使用每个线程一个channel 的模型。不过,一个客户端在一个或多个AMQP服务端上打开多个连接也是可以的。
Channel的生命周期为:
Exchange类能够让应用操作服务端的交换器。这个类能够让程序自己设置路由,而不是通过某些配置。不过大部分程序并不需要这个级别的复杂度,过去的中间件也不只支持这个语义。
Exchange的生命周期为:
该类用于让程序管理服务端上的消息队列。几乎所有的消费者应用都是基本步骤,至少要验证使用的消息队列是否存在。
一个持久化消息队列的生命周期非常简单
一个临时消息队列的生命周期会更有趣些:
AMQP实现了Topic订阅的分发模型。这可以让订阅在合作的订阅者间进行负载均衡。涉及到额外的绑定阶段的生命周期:
Basic实现本规范中描述的消息功能。支持如下语义:
AMQP支持两种类型的事务:
Transaction 类(“tx”) 使应用程序可访问第二种类型,即服务器事务。这个类的语义是:
事务包含发布消息和ack,不包含分发。所以,回滚并不能重入队列或者重新分发任何消息。客户端有权在事务中确认这些消息。
AMQP的功能描述,一定程度上也是RabbitMQ的功能描述,不过RabbitMQ基于AMQP做了一些扩展
消息会携带一些属性,以及具体内容(二进制数据)
消息是可被持久化的。持久化消息是可以安全的存在硬盘上的,即使发生了验证的网络错误、服务端崩溃溢出等情况,也可以确保被投递。
消息可以有优先级。同一个队列中,高优先级的消息会比低优先级的消息先被发送。当消息需要被丢弃时(比如服务端内存不足等),将会优先丢弃低优先级消息
服务端一定不能修改消息的内容。但服务端可能会在消息头上添加一些属性,但一定不会移除或者修改已经存在的属性。
虚拟主机是服务端的一个数据分区。在多租户使用是,可以方便进行管理。
虚拟主机有自己的命名空间、交换器、消息队列等等。所有连接,只可能和一个虚拟主机建立。
交换器是一个虚拟主机内的消息路由Agent。用于处理消息的路由信息(一般是Routing-Key),然后将其发送到消息队列或者内部服务中。交换器可能是持久化的、临时的、自动删除的。交换器把消息路由到消息队列时可以是并行的。这会创建一个消息的多个实例。
RabbitMQ常用的交换器类型有direct、topic、fanout、headers四种:
Time To Live,也就是生存时间,是一条消息在队列中的最大存活时间,单位是毫秒,下面看看RabbitMQ过期时间特性:
为了保证消息从队列可靠地到达消费者,RabbitMQ提供了消息确认机制。消费者订阅队列的时候,可以指定autoAck参数,当autoAck为true的时候,RabbitMQ采用自动确认模式,RabbitMQ自动把发送出去的消息设置为确认,然后从内存或者硬盘中删除,而不管消费者是否真正消费到了这些消息。当autoAck为false的时候,RabbitMQ会等待消费者回复的确认信号,收到确认信号之后才从内存或者磁盘中删除消息。
消息确认机制是RabbitMQ消息可靠性投递的基础,只要设置autoAck参数为false,消费者就有足够的时间处理消息,不用担心处理消息的过程中消费者进程挂掉后消息丢失的问题。
消息的可靠性是RabbitMQ的一大特色,那么RabbitMQ是如何保证消息可靠性的呢?答案就是消息持久化。持久化可以防止在异常情况下丢失数据。RabbitMQ的持久化分为三个部分:交换器持久化、队列持久化和消息的持久化。
交换器持久化可以通过在声明队列时将durable参数设置为true。如果交换器不设置持久化,那么在RabbitMQ服务重启之后,相关的交换器元数据会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器了。
队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是不能保证内部所存储的消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。队列的持久化可以通过在声明队列时将durable参数设置为true。
设置了队列和消息的持久化,当RabbitMQ服务重启之后,消息依然存在。如果只设置队列持久化或者消息持久化,重启之后消息都会消失。
当然,也可以将所有的消息都设置为持久化,但是这样做会影响RabbitMQ的性能,因为磁盘的写入速度比内存的写入要慢得多。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。鱼和熊掌不可兼得,关键在于选择和取舍。在实际中,需要根据实际情况在可靠性和吞吐量之间做一个权衡。
当消息在一个队列中变成死信之后,他能被重新发送到另一个交换器中,这个交换器成为死信交换器,与该交换器绑定的队列称为死信队列。消息变成死信有下面几种情况:
DLX也是一个正常的交换器,和一般的交换器没有区别,他能在任何的队列上面被指定,实际上就是设置某个队列的属性。当这个队列中有死信的时候,RabbitMQ会自动将这个消息重新发送到设置的交换器上,进而被路由到另一个队列,我们可以监听这个队列中消息做相应的处理。
死信队列有什么用?当发生异常的时候,消息不能够被消费者正常消费,被加入到了死信队列中。后续的程序可以根据死信队列中的内容分析当时发生的异常,进而改善和优化系统。
一般的队列,消息一旦进入队列就会被消费者立即消费。延迟队列就是进入该队列的消息会被消费者延迟消费,延迟队列中存储的对象是的延迟消息,“延迟消息”是指当消息被发送以后,等待特定的时间后,消费者才能拿到这个消息进行消费。
延迟队列用于需要延迟工作的场景。最常见的使用场景:淘宝或者天猫我们都使用过,用户在下单之后通常有30分钟的时间进行支付,如果这30分钟之内没有支付成功,那么订单就会自动取消。除了延迟消费,延迟队列的典型应用场景还有延迟重试。比如消费者从队列里面消费消息失败了,可以延迟一段时间以后进行重试。
这里才是内容的重点,不仅需要知道Rabbit的特性,还需要知道支持这些特性的原因:
AMQP简单来说就是规定好了MQ的各个抽象组件,RabbitMQ则是一款完全严格按照AMQP来实现的开源MQ,使得很好被开源框架所集成,比如Spring AMQP专门就是用来操作AMQP架构的中间件的,因此RabbitMQ可以被Spring Boot很方便的集成。
RabbitMQ也是三大MQ里提供的消息模型最丰富的一种MQ。
简单队列,consumer和producer通过队列直连。
工作队列(work queue),让多个消费者去消费同一个消息队列中的消息,支持轮询分发(默认)、公平分发两种分发模式。
订阅模式(fanout),也叫广播模式,见名知意,其特点是将消息广播出去。通过交换机将生产者生产的消息分发到多个队列中去,从而支持生产者生产的一个消息被多个消费者消费。
路由模式(direct),在订阅模式支持一条消息被多个消费者消费的特性上增加了分类投递的特性,通过交换机,支持消息以类别(routing key)的方式投送到不同的消息队列中去。
在路由模式以类别进行消息投送的基础上增加了对通配符的支持,这样就可以使用通配符将多个类别聚合成一个主题。
远程调用不太算MQ
RabbitMQRabbitMQ 提供了多种机制来确保消息的可靠性,包括持久化、消息确认、发布确认等。这些机制确保消息不会丢失,并且能够在各种情况下处理消息传递失败。但是由于存在这些用于保证消息可靠性的机制,RabbitMQ的吞吐量在三大中间件中是最低的。
ActiveMQ | Kafka | RocketMQ | RabbitMQ | |
---|---|---|---|---|
开发语言 | Java | Scala | Java | Erlang |
客户端SDK | Java,.NET,C++等 | Java,Scala等 | Java,C++,Go | Java,.NET,PHP,Python,JavaScript,Ruby,Go等 |
吞吐量 | 万级,同rabbitmq差不多 | 10万级(17.3w/s),高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | 10万级(11.6w/s),支持高吞吐 | 万级(5.95w/s)为保证消息可靠性在吞吐量上做了取舍 |
topic数量对吞吐量的影响 | topic从几十到几百个时候,吞吐量会大幅下降,在同等机器下,Kafka尽量保证topic数量不要过多,如果要支撑大规模的topic,需要增加更多的机器资源 | topic可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic | ||
时效性 | 毫秒级 | 毫秒级 | 毫秒级 | 微秒级,RabbitMQ的一大特点,延迟最低 |
可用性 | 高,主从架构 | 非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | 非常高,分布式架构 | 高,基于主从架构实现高可用性 |
消息可靠性 | 有较低的概率丢失数据 | 经过参数优化配置可做到0丢失 | 经过参数优化配置,可以做到0丢失 | 通过消息确认,持久化等手段保持消息可靠 |
性能稳定性 | 队列/分区多时性能不稳定,明显下降。消息堆积时性能稳定 | 队列较多,消息堆积时性能稳定 | 消息堆积时,性能不稳定、明显下降 | |
协议和规范 | Push model,support,OpenWire,STOMP,AMQP,MQTT,JMS | Pull model,support TCP | Pull model,support TCP,JMS,OpenMessaging | Push model,support,AMQP,XMPP,SMTP,STOMP |
定时/延时推送 | 支持的 | 不支持 | 支持的 | 不支持 |
有序消息 | 独立消费者或独立队列可以保证消息有序 | 保证独立分区内消息的顺序,但是一台Broker宕机后,就会产生消息乱序 | 保证独立分区内消息的顺序 | |
批量发送 | 不支持 | 支持,带有异步生产者 | 严格确保消息有序,并可以优雅的拓展 | 不支持 |
广播消息 | 支持的 | 不支持 | 支持的 | 支持的 |
消息过滤 | 支持的 | 支持,可以使用KafkaStreams过滤消息 | 支持基于SQL92的属性过滤器表达式 | 不支持 |
消息重发 | 不支持 | 支持的 | 支持的 | 支持的 |
消息存储 | 使用JDBC和高性能日志(例如leveIDB,kahaDB)支持非常快速的持久化 | 高性能文件存储 | 高性能和低延迟的文件存储 | 高性能文件存储 |
消息回溯 | 支持的 | 支持按照偏移量来回溯消息 | 支持按照时间来回溯消息 | 不支持 |
消息优先 | 支持的 | 不支持 | 不支持 | 支持的 |
讯息轨道追踪 | 不支持 | 不支持 | 支持的 | 支持的 |
配置 | 默认配置为低级别,用户需要优化配置参数 | Kafka使用键值对格式进行配置。这些值可以从文件或以编程方式提供 | 开箱即用,用户只需要注意一些配置 | 使用键值对格式进行配置。这些值可以从文件或以编程方式提供 |
管理和操作工具 | 支持的 | 支持,使用终端命令公开核心指标 | 支持web和终端命令可显示核心指标 | 支持web和终端命令可显示核心指标 |
高可用性和故障转移 | 支持,取决与存储,如果使用kahadb,则需要ZooKeeper服务器 | 支持,需要ZooKeeper服务器 | 支持主从模式,无需其他套件 | 支持主从模式,无需其他套件 |
成熟度 | 成熟 | 成熟日志领域 | 比较成熟 | 成熟 |
特点 | 功能齐全,被大量开源项目使用 | 用于大数据领域实时计算、日记采集等topic和消费端都较少的弱业务性场景 | 各个环节分布扩展设计,主从HA,支持上万个队列多种消费模式,性能很好 | 由于Erlang语言的并发能力,性能很好 |
持久化 | 内存、文件、数据库 | 文件、磁盘 | 磁盘文件 | 内存、文件 |
事务 | 支持 | 支持 | 支持 | 支持 |
负载均衡 | 支持 | 支持 | 支持 | 支持 |
安装要点
Erlang与RabbitMQ,安装路径都应不含空格符。
Erlang使用了环境变量HOMEDRIVE与HOMEPATH来访问配置文件.erlang.cookie,应注意这两个环境变量的有效性。需要设定环境变量ERLANG_HOME,并把%ERLANG_HOME%\bin加入到全局路径中。
RabbitMQ使用本地computer name作为服务器的地址,因此需要注意其有效性,或者直接解析为127.0.0.1
可能需要在本地网络防火墙打开相应的端口。