RabbitMQ是一个开源的消息代理和队列服务器,用来在不同的应用之间共享数据。1983年,被认为是RabbitMQ的雏形的Teknekron创建,首次提出了消息总线的概念。中间经历过数个阶段的发展,一直到2004年,AMQP(Advanced Message Queuing Protcol)协议开发完成,这是一款具有开放标准的通信协议,它允许任何人都可以执行这一标准,编码标准的任何人都可以和任何AMQP供应商提供的MQ服务器进行交互。直到2007年,RabbitMQ 1.0正式诞生,RabbitMQ开始在金融、电信领域大面积的使用。
RabbitMQ使得生产者和消费者真正做到了解耦。引入了代理服务器,可以实现高可用和弹性的横向拓展。通过增加服务器实例的方式,RabbitMQ可以做到每秒处理百万级别的消息。本文主要介绍RabbitMQ的逻辑体系结构,先是简单介绍其整个体系架构以及相关名词的介绍,然后介绍RabbitMQ三种消息路由的方式(因第四种Header模式基本不会使用,这里不包括在内),最后介绍其典型的应用场景。
一、RabbitMQ的体系架构
我们知道RabbitMQ最初被用来主要是做应用之间的解耦,所以它的整体的架构大概分为三个部分:生产者(Publisher),消费者(Consumer)和消息代理(Broker)。其中最核心的是消息代理,其内部结构稍显复杂,主要针对不同的应用场景,实现不同的消息处理逻辑。我们先来看下RabbitMQ的逻辑架构图:
下面,分别对这些名词进行解释:
Publisher:消息的生产者
Broker:消息代理,通常是一个服务实体。
VHost:Virtual Host,主要用来隔离一批交换机、队列,在每个虚拟主机中都可以创建自己的交换机、队列和绑定,虚拟主机之间互相不可见。
Connection:一个网络连接
Channel:信道,AMQP中引入了信道进行消息的传递。主要是因为操作系统中创建和销毁一个TCP连接代价非常昂贵,信道的引入就是为了复用一个TCP连接,通过多线程的方式来传递消息。通常情况下,一个信道对应一个线程。
Exchange:用来接受消息,并负责把消息传递给队列。
Queue:用来存放消息的容器。
Binding:用来关联交换机和队列的绑定,只有绑定了之后,交换机才知道向哪个队列发送消息。
二、RabbitMQ的路由策略
根据交换机的类型,RabbitMQ的的路由策略可以分为三种:直连默认(Direct)、主题模式(topic)、广播模式(fanout)。接下来,我们分别进行介绍。不过,在此之前,我们需要先了解下RabbitMQ的消息发布的命令,因为结合这个命令,我们才能购真正理解RabbitMQ这几种路由策略的原理,笔者以Java代码的API来解释这个命令:
channel.basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body);
exchange:交换机的名称,在直连模式下为空。
routingKey:路由键,直连模式下是队列的名称,主题模式下是主题的名称,广播模式下为空。
props:发送消息时附带的一些附加参数
body:发送的消息转换成的二进制数组
2.1 直连模式
所谓的直连模式,就是直连交换机直接将消息路由到对应的队列中。在直连模式下,使用basicPublish在publish消息时,routingKey必须设置成目标队列的名称。而exchange可以设置成我们创建的直连交换机的名称,如果设置则使用RabbitMQ安装时默认的直连交换机。
要想使用直连模式路由消息,必须先创建直连交换机。当安装完成RabbitMQ后,会默认创建1个直连交换机(下图中圈出部分),如果我们在basicPublish时不指定交换机名称,会使用这个默认交换机(直连模式下):
除此之外,我们也可以创建支持直连模式的交换机,如下:
channel.exchangeDeclare("my-direct-exchange", "direct", true, false, null);
如上,我们创建了一个名称为my-direct-exchange的交换机,第二个参数指定了交换机的类型为direct类型。完成了交换机的创建之后,接下来创建一个队列my-queue用来接受交换机的消息:
channel.queueDeclare("my-queue", true, false, false, null);
最后,创建一个binding,将交换机和队列关联起来:
channel.queueBind("my-queue", "my-direct-exchange", "my-queue");
queueBind的第1个参数是要关联的队列名称,第2个参数是要关联的交换机名称,第三个参数是路由键,在直连模式下要填写队列名称。
完成了上述的过程之后,我们就创建了一套完整的直连交换机、队列和绑定,实现了以direct的方式路由消息。发布消息时,路由键填写队列名称:
channel.basicPublish("direct-exchange", "my-queue",...);
2.2 主题模式
RabbitMQ的第二种路由模式是topic模式,topic类型的交换机会根据发送消息的topic寻找使用binding绑定了该topic的的队列,然后将消息路由到该队列上。
需要注意的是,这种交换机和队列的关系是一对多的,也就是是说,多个队列可以通过同一个主题和同一个交换机绑定:
使用主题模式的路由策略,同样先创建topic类型的交换机:
channel.exchangeDeclare("topic-exchange", "topic", true, false, null);
topic-exchange:交换机名称
topic:交换机类型,固定是topic
队列的创建,这里不再演示,因为队列没有direct、topic和fanout类型之分,我们还是沿用上面创建的队列my-queue来进行接下来的说明。与direct路由策略的bind不同,在为topic交换机和队列创建bind时要明确得指出主题:
channel.queueBind("my-queue", "topic-exchange", "fruit");
可以看到,在第3个参数路由键中,我们明确得指出了topic为fruit。
然后,在发布消息时,也要明确得指出消息的topic:
channel.basicPublish("topic-exchange", "fruit",...);
2.3 广播模式
RabbitMQ最后一种路由策略是fanout模式,该模式的路由原理如下图:
在该模式下,只要将队列和fanout交换机进行了绑定,那么向fanout交换机发布的所有的消息都会路由到所有绑定的队列中。与之前创建两种类型的交换机类似,创建fanout交换机时,需要指定交换机的类型为fanout:
channel.exchangeDeclare("fanout-exchange", "fanout", true, false, null);
在创建bind时不需要指定路由键:
channel.queueBind("my-queue", "fanout-exchange", "");
在发布消息时,同样不需要指定路由键,fanout 交换机会把消息发送到所有绑定它上面的队列中:
channel.basicPublish("fanout-exchange", "", ...);
2.4 关于Exchange、Queue和Bind之间的关系
可能刚刚接触RabbitMQ的童鞋,会被这三者的关系绕晕。我最开始学习RabbitMQ时,也是花了很长时间才把这三者之间的关系梳理清楚。笔者这里所说的关系,是指1个Exchange对应1个Queue,或者1个Exchange对应多个Queue等这种数量上的对应关系。针对这种关系,最终我发现了一个事实,总结如下:
(1)三者之间这种数量上的对应关系和RabbitMQ的路由策略以及Exchange的类型无关。只要保证一个bind对应在创建时绑定了一个exchange和一个queue即可。
(2)一个exchange可以和多个queue通过多个bind对象绑定,不管exchange是什么类型。
(3)一个queue可以和多个exchange通过多个bind对象绑定,不管exchange是什么类型。
三、RabbitMQ的应用场景
笔者,根据RabbitMQ的特性和使用目的,将RabbitMQ的应用场景归为了三类:
将消费者和生产者解耦。
这是RabbitMQ被提出来最基本也是最初的目的。通过这种解耦,至少可以实现两种效果:
(1)原本需要轮询获取生产者消息的消费者代码,可以通过监听RabbitMQ队列实现有消息才处理,从而省去了轮询的过程。
(2)通过RabbitMQ主题交互机或者广播交换机,可以轻松实现当有新的消费者加入时,而不需要修改生产者代码。比如在充值时,我们可以发送一个充值事件,日志服务捕获充值事件后写入日志。如果后面有新的需求,比如积分服务,只需要新增积分服务即可,无需修改生产者代码。
实现分布式事务
实现分布式事务。利用RabbitMQ代理可持久化,可做高可用以及消息的ack机制,可以实现分布式事务,比如saga模式,ebay的本地事务表。
实现异步调用,提高性能。
在微服务架构的系统种,要实现某种业务往往需要调用多个其它服务的接口,如果采用同步调用的方式,需要在上一个接口调用完成之后再去调用下一个接口,性能势必不高。如果使用RabbitMQ作为代理进行异步调用,几乎可以实现和只调用一个接口同样的耗时。而且采用这种方式,还有一个好处,就是可以有选择地忽略完成该业务非必要地接口调用。
四、总结
本文主要介绍了RabbitMQ的体系结构、三种路由模式和应用场景,结合上文,现总结如下:
(1)RabbitMQ中通过vhost实现了数据的隔离,每个vhost下都有自己的exchange和queue,可以以vhost为单位对用户进行授权。
(2)RabbitMQ中有三种路由策略:
direct策略 —— 需要创建direct exchange,和queue绑定时指定路由键为队列名称,在发布消息时需要以目标队列的名称作为路由键。
topic策略 —— 需要创建topic exchange,和queue绑定时需要将明确的topic作为路由键,发布消息时需要将topic作为路由键。
fanout策略 —— 需要创建fanout exchange,和queue绑定时无需指定路由键,发布消息时无需指定路由键。
(3)一个exchange可以和多个queue通过多个bind对象绑定,不管exchange是什么类型。
(4)一个queue可以和多个exchange通过多个bind对象绑定,不管exchange是什么类型。