RabbitMQ剖析:消息中间件的工作原理和使用

2,284 阅读11分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情

概述

主流的消息中间件主要有:ActiveMQKafkaRabbitMQRocketMQ等等......,而我们今天的主角是:RabbitMQRabbitMQ基于AMQP协议来实现的,AMQP的和主要特征是面向消息、队列、路由(包括点对点和发布 / 订阅)、可靠性、安全,RabbitMQ支持多种语言,有消息确认机制和持久化机制,保证数据不丢失的前提做到可靠性、可用性。另外,如今同类型组件市场上,RabbitMQ也占有率还是比较高,也是很多中小厂比较青睐的一款消息中间件产物,无非得益于它性能好、容易上手等优点。

最近有想写做一个消息队列专栏,总结记录自己在使用消息队列实战经验顺便做一些分享,本篇文章主要讲述RabbitMQ的底层实现原理,对于新手来说能起到很好的了解作用,对于使用过MQ的开发者,也能起到一定的帮助,在介绍RabbitMQ之前,有必要先了解一下什么是AMQP协议。

什么是AMQP协议?

AMQP的全称:Advanced Message Queuing Protocol(高级消息队列协议),它是消息队列的一个规范,其中定义个很多核心的概念,AMQPJMS(Java Message Service)Java平台的专业技术规范类似,同样提供了很多面向中间件的API,用于两个应用程序之间,或者分布式系统之间的发送消息,进行异步通信。

AMQP核心概念

AMQP组件说明
Server又称Broker,接受客户端的连接,实现AMQP实体服务
Connection连接,应用于程序与Broker的网络连接
Channel网络通道,几乎所有的操作都是在channel中进行的,channel是进行消息读写的通道,客户可以建立多个channel,每个channel代表一个会话任务
Message消息,服务器与应用程序之间传送的数据,有PropertiesBody组成,Properties可以对消息进行修饰,比如消息的优先级,延迟等高级特性,Body是消息体内容
Virtual host虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个Virtrual host里面可以有若干个ExchangeQueue,用一个Virtrual host里面不能有相同名字的ExchangeQueue
Exchange交换机,接收消息,根据路由键转发消息到绑定的队列
Routingkey生产者架构消息发给交换器的时候,会指定一个RoutingKey,用来指定这个消息的路由规则,通过RoutingKey来决定消息流向哪里
Binding绑定,RabbitMQ中通过绑定将交换器跟队列关联起来,在绑定的时候会指定一个BindingKey,这样RabbitMQ就知道如何正确的将消息路由到对应的队列中去了,也就是生产者将信息发送给交换器时,需要一个RoutingKey,当RoutingKeyBindingKey完全匹配时,消息会被路由到对应的队列中去
Queue全名Message Queue,消息队列,保存消息并将他们转发给消费者

工作模型

image.png

  • 生产者只需要将消息投递到Exchange交换机中,不需要关注消息被投递到哪个队列。
  • 消费者只需要监听队列来消费消息,不需要关注消息来自于哪个Exchange
  • ExchangeMessage Queue存在着绑定的关系,一个Exchange可以绑定多个消息队列。

🌈友情解释:对于初学者,交换器、路由键、绑定这几个概念理解起来比较晦涩,这里做个比喻:交换器相当于投递包裹的邮箱,RoutingKey相当于填写在包裹上的地址,BindingKey相当于包裹的目的地,当填写在包裹上的地址和实际想要投递的地址相匹配那么这个包裹就会被投递到目的地,最后这个目的地的主人 "队列" 就可以保留这个包裹,如果对应的地址不匹配,也就是RoutingKeyBindingKey不匹配,邮递员就不能正确的投递到目的地,包裹可能会回退给寄件人,也可能被丢弃。

AMQP消息路由

AMQP中消息的路由过程和JMS存在一些差别。AMQP中增加了ExchangeBinding的角色。生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收,而Binding决定交换器的消息应该发送到哪个队列。如下图所示:

image.png

Exchange交换机类型

RabbitMQ常用的交换器类型主要有四种:directfanouttopicheadersExchange分发消息时根据类型的不同分发策略有区别,headers匹配AMQP消息的header而不是路由键,此外headers交换器和 direct交换器完全一致,但性能差很多,目前几乎用不到了,这里仅仅对其他三种进行展开说明:

1. direct

image.png

direct类型的交换机路由规则需要遵循严格的完全匹配规则,它会把消息路由到那些BindingKeyRoutingKey完全匹配的队里中,如果消息中的路由键(RoutingKey)如果和Binding中的BindingKey一致, 交换器就将消息发到对应的队列中。比如:

image.png

Tips:在发送消息的时候设置路由键为info或者debug,消息只会路由到Queue2键,如果以其他路由键发送消息,则消息不会路由到这两个队里中,这就是路由键和Binding key`的完全匹配。

direct类型模式的特点:

1️⃣ 不需要将Exchange进行任何绑定(binding)操作。
2️⃣ 消息传递时需要一个RoutingKey,可以简单的理解为要发送到的队列名字。
3️⃣ 如果vhost中不存在RoutingKey中指定的队列名,则该消息会被抛弃。

2.fanout

image.png

fanout类型的交换机会将所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。fanout交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout类型转发消息是最快的。

这种模式的特点:

1️⃣ 不需要RoutingKey,我们可以将路由键设置为空即可。
2️⃣ 需要提前将ExchangeQueue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同时与多个Exchange进行绑定("多对多关系")。
3️⃣ 如果接收到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。

3.topic

image.png

topic类型的交换器理由规则需要遵循严格的完全匹配规则,这种严格的匹配方式有时候不能满足实际的业务需求,topic就是在这种规则上进行了扩展,topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,但是这里的匹配规则有所不同,它的约定如下:

  • outingKey为一个点号 ".",分隔的字符串(被点号 "." 号分隔开的一段独立的字符串称为单词),举个栗子🌰比如:com.rabbit.clientjava.util.Map;
  • BindingKeyRoutingKey为一个点号 ".",分隔的字符串;
  • BindingKey中可以存在两种特殊的字符串 "" 和 "#",用于做模糊匹配,其中 "" 用于匹配一个单词, "#" 用于匹配多规则单词;
    • com.#可以匹配到com.rabbitmq.aaa
    • com.*可以匹配到com.rabbitmq

image.png

  • 路由键为com.rabbitmq.client的消息会同时路由到Queue1Queue2
  • 路由键为com.hidden.client的消息会只会路由到Queue2
  • 路由键为com.hidden.data的消息会只会路由到Queue2
  • 路由键为java.rabbitmq.data的消息会只会路由到Queue1
  • 路由键为java.util.map的消息将会被丢弃或者返回给生产者,因为它没有匹配任何的路由键。

RabbitMQ特性

RabbitMQ最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:

  1. 可靠性: RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
  2. 灵活的路由(Flexible Routing): 在消息进入队列之前,通过Exchange来路由消息的。对于典型的路由功能,RabbitMQ已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange
  3. 消息集群(Clustering): 多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker
  4. 高可用: 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
  5. 多种协议(Multi-protocol): RabbitMQ支持多种消息队列协议,比如STOMP、MQTT等等。
  6. 多语言客户端: RabbitMQ几乎支持所有常用语言,比如Java、.NET、Ruby等等。
  7. 管理界面: RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。
  8. 跟踪机制: 如果消息异常,RabbitMQ提供了消息跟踪机制,使用者可以找出发生了什么。
  9. 插件机制: RabbitMQ提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。。

RabbitMQ 中的概念模型

消息模型

消费者(consumer)订阅某个队列。生产者(producer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者。

RabbitMQ基本概念

上面只是最简单抽象的描述,具体到RabbitMQ则有更详细的概念需要解释。上面介绍过RabbitMQAMQP协议的一个开源实现,下图演示了其内部结构:

RabbitMQ 的作用和使用场景

RabbitMQ 的核心组件

Hello RabbitMQ World

“Hello RabbitMQ World!”, 学习一门技术,先出hello world开始,我们来编写一个Java项目来使用 RabbitMQ来实现消息的生产和消费,这样能让我们能够更好的理解RabbitMQ的作用和原理,RabbitMQ是消息代理,它负责接收并转发消息,我们可以将它视为邮局,将要发送的邮件都放在邮箱中,可以确保 Mailperson 先生或者女生最终将邮件传递给别人,因此:RabbitMQ是一个邮箱,一个邮局和一个邮递员的实例。

  • 生产意味着发送,发送消息的程序是生产者
  • 队列就是 RabbitMQ 内部的邮箱名称,消息是存储在队列中的,尽管消息流经 RabbitMQ 和你的应用程序,生产者可以发送一个队列信息,许多消费者可以尝试从一个队列里接收数据
  • 消费与接受者是同一个身份,一个消费者是一个程序,主要是等待接收信息

pom 依赖

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

我们称其为消息发布者(发送者)MessageProducer和我们的消息消费者(接收者)MessageConsumer。发布者将连接到RabbitMQ,发送响应的消息:

MassageProducer (消息生产者)

public class MassageProducer {

    private final static String QUEUE_NAME = "myQueue";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        //设置RabbitMQ所在主机的ip或者主机名
        factory.setHost("localhost");
        //设置端口号
        factory.setPort(5672);
        Connection conn = factory.newConnection();
        //创建一个通道
        Channel channel =  conn.createChannel();
        //指定一个队列
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //定义要发送的消息
        String message = "Hello RabbitMQ World!";
        //往队列里发送message消息
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
        System.out.println(" [生产者] 发送消息: '" + message + "'");
        }
}

MessageConsumer (消息消费者)

public class MessageConsumer {

    private final static String QUEUE_NAME = "myQueue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println("消费者正在等待消息,退出请按CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [消费者] 接收到了: '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

在运行程序之前我们需要先启动RabbitMQ服务,前提需要安装好ErlangRabbitMQ,这里安装的教程就不展开说明了:

下载 Erlang 软件包 (这个网址下载速度比较快)

RabbitMQ 官网

  • 注意的是:这里必须下载安装 Erlang 安装包,因为 RabbitMQ 是基于 Erlang 进行开发的。

启动服务之后,如图所示:

接着我们运行我们的 MessgeProducer.java 和 MessageConsumer.java,先运行 MessgeProcucer,接着运行 MessageConsumer,观察控制台的输出:

 

至此,我们的第一个 RabbitMQ 服务程序编程测试成功了 !

RabbitMQ web 客户端

启动之后,我们可以登录 RabbitMQ 的 web 客户端查看我们的信息:http://localhost:15672/, 通过客户端我们可以查看 RabbitMQ 的版本信息,连接信息,Channel、Exchange 连接等等...

我们也可以通过客户端新增 Exchange 并指定类型:

同样也可以手动添加队列,具体操作我们可以自己尝试登陆客户端来设置!

总结

本篇文章主要是介绍了RabbitMQ消息中间件的使用原理,以及通过一个简单的示例演示了消息投递到消费的过程,本篇文章对于一开始想了解MQ入门级的开发者来说应该会有帮助,好了,今天的分享就到此结束了,我是:👨‍🎓austin流川枫,我们下期见!