RabbitMQ
文章目录
RabbitMQ是一个消息代理:它接受并转发消息。您可以将其视为邮局:当您将要投递的邮件放入邮箱时,您可以确定信件承运人最终会将邮件传递给您的收件人。在这个类比中,RabbitMQ是一个邮筒,一个邮局和一个信件载体。
RabbitMQ和邮局之间的主要区别在于它不处理纸张,而是接受,存储和转发二进制数据 blob - 消息。
消息队列的概念
消息队列 (Message Queue)是一种消息的容器,主要用于实现程序(服务、进程、线程)之间的通信
队列 是 FIFO(先进先出)的数据结构
消息队列的作用
1. 解耦
让生产者服务(消息发送者)和消费者服务(消息接收者)进行解耦
举一个通俗的例子:
在我们需要寄快递的时候,按照第一种调用的方式就是我们需要和快递员直接联系,这样耦合性就很大,我们的行动需要和快递员匹配。按照第二种消息队列的方式就是在我们与快递员之间加了一个菜鸟驿站,我们就可以把快递直接交给菜鸟驿站,然后快递员再从菜鸟驿站取到快递。这样就使我们与快递员的行动之间的耦合性降低
2. 异步:
服务消费者如果需要调用多个提供者的接口,同步方式需要比较多的时间
异步方式:服务提供者给消息队列发送消息,不用等待消费者返回响应,响应速度大大提升
很多时候,我们需要在某个接口中,调用其他服务的接口。
比如有这样的业务场景:
在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息。
而用户名称、性别、等级、头像在用户服务中,积分在积分服务中,成长值在成长值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。
于是,用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。
调用过程如下图所示:
调用远程接口总耗时 530ms = 200ms + 150ms + 180ms
显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。
那么如何优化远程接口性能呢?
上面说到,既然串行调用多个远程接口性能很差,为什么不改成并行呢?
如下图所示:
调用远程接口总耗时 200ms = 200ms(即耗时最长的那次远程接口调用)
3. 削峰
消息队列可以设置消息最大个数,超过部分不进行处理。可以处理流量激增的情况
RabbitMQ的优势
**
可靠性(Reliablity):
**使用了一些机制来保证可靠性,比如持久化、传输确认、发布确认。**
灵活的路由(Flexible Routing):
**在消息进入队列之前,通过Exchange来路由消息。对于典型的路由功能,Rabbit已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange。**
消息集群(Clustering):
**多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。**
高可用(Highly Avaliable Queues):
**队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。**
多种协议(Multi-protocol):
**支持多种消息队列协议,如STOMP、MQTT等。**
多种语言客户端(Many Clients):
**几乎支持所有常用语言,比如Java、.NET、Ruby等。**
管理界面(Management UI):
**提供了易用的用户界面,使得用户可以监控和管理消息Broker的许多方面。**
跟踪机制(Tracing):
**如果消息异常,RabbitMQ提供了消息的跟踪机制,使用者可以找出发生了什么。**
插件机制(Plugin System):
**提供了许多插件,来从多方面进行扩展,也可以编辑自己的插件。
主流的消息队列
特性(feature) | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
多语言支持 | 支持(JAVA优先) | 语言无关 | 只支持JAVA | 支持(JAVA优先) |
单机吞吐量 | 万级 | 万级 | 十万级 | ★十万级 |
消息延迟 | 毫秒级 | ★微秒级 | 毫秒级 | 毫秒级 |
可用性 | 高(主从) | 高(主从) | 高(分布式) | 高(分布式) |
消息丢失 | 低 | 低 | 理论上不会丢失 | 理论上不会丢失 |
消息重复 | 可控 | 可控 | ||
部署难度 | 低 | 中 | ||
商业支持 | 阿里云 | |||
特点 | 功能齐全 | 并发能力强,性能高 | 性能高 | 并发能力强 |
顺序消息 | 不支持 | 不支持 | 支持 | 支持 |
负载均衡 | 支持负载均衡。基于zookeeper实现负载均衡。 | 负载均衡的支持不好 | 支持 | 支持 |
管理界面 | 好 | 好 | 有管理后台, 但不是项目里自带, 需要自己启动一个单独的管理后台实例 | 一般 |
安装配置过程
[]:
基本概念
- Broker:即消息队列服务器实体。
- Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
- Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
- Binding:绑定,它的作用是把 Exchange 和 Queue 按照路由规则绑定起来。
- Routing Key:路由关键字,Exchange 根据这个关键字进行消息投递。
- Vhost:虚拟主机,一个 Broker 里可以开设多个 Vhost,用作不同用户的权限分离。
- Producer:消息生产者,就是投递消息的程序。
- Consumer:消息消费者,就是接受消息的程序。
- Channel:消息通道,在客户端的每个连接里,可建立多个 Channel,每个 Channel 代表一个会话任务。
RabbitMQ的优缺点
优点主要有以下几点:
- 由于 Erlang 语言的特性,RabbitMQ 性能较好、高并发;
- 健壮、稳定、易用、跨平台、支持多种语言客户端、文档齐全;
- 有消息确认机制和持久化机制,可靠性高;
- 高度可定制的路由;
- 管理界面较丰富,在互联网公司也有较大规模的应用;
- 社区活跃度高,更新快。
缺点主要有:
- 尽管结合 Erlang 语言本身的并发优势,性能较好,但是不利于做二次开发和维护;
- 实现了代理架构,意味着消息在发送到客户端之前可以在中央节点上排队。此特性使得 RabbitMQ 易于使用和部署,但使得其运行速度较慢,因为中央节点增加了延迟,消息封装后也比较大;
- 需要学习比较复杂的接口和协议,学习和维护成本较高。
基本使用
安装完成RabbitMQ之后,启动,可以通过浏览器访问 http://localhost:15672 来进入RabbitMQ的管理界面,默认登录账号密码都是: guest
用户
不同的系统可以使用各自的用户登录RabbitMQ,可以在Admin的User页面添加新用户
虚拟主机
虚拟主机相当于一个独立的MQ服务,有自身的队列、交换机、绑定策略等。 添加虚拟主机
队列
不同的消息队列保存不同类型的消息,如支付消息、秒杀消息、数据同步消息等。 添加队列,需要填写虚拟主机、类型、名称、持久化、自动删除和参数等。
交换机
消息队列的使用过程
- 客户端连接到消息队列服务器,打开一个 Channel。
- 客户端声明一个 Exchange,并设置相关属性。
- 客户端声明一个 Queue,并设置相关属性。
- 客户端使用 Routing Key,在 Exchange 和 Queue 之间建立好绑定关系。
- 客户端投递消息到 Exchange。Exchange 接收到消息后,根据消息的 Key 和已经设置的 Binding,进行消息路由,将消息投递到一个或多个队列里。
- 有三种类型的 Exchange,即 Direct、Fanout、Topic,每个实现了不同的路由算法(Routing Algorithm)。
Direct Exchange
:完全根据 Key 投递。如果 Routing Key 匹配,Message 就会被传递到相应的 Queue 中。其实在 Queue 创建时,它会自动地以 Queue 的名字作为 Routing Key 来绑定 Exchange。例如,绑定时设置了 Routing Key 为“abc”,那么客户端提交的消息,只有设置了 Key为“abc”的才会投递到队列中。
Fanout Exchange
:该类型 Exchange 不需要 Key。它采取广播模式,一个消息进来时,便投递到与该交换机绑定的所有队列中。
Topic Exchange
:对 Key 进行模式匹配后再投递。比如符号“#”匹配一个或多个词,符号“.”正好匹配一个词。例如“abc.#”匹配“abc.def.ghi”,“abc.”只匹配“abc.def”。
消息模型
1. 简单的一对一模型
在下图中,“P”是我们的生产者,“C”是我们的消费者。中间的框是一个队列 - RabbitMQ 代表使用者保留的消息缓冲区。
我们将消息发布者(发送者)称为 Send,将消息使用者(接收方)称为 Recv。发布者将连接到 RabbitMQ,发送一条消息,然后退出。
代码实现:
- 创建spring boot 项目,引入rabbitMQ依赖.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
- 创建消息发布者Send.class:
public class Send { // 命名队列 public static final String QUEUE_NAME = "QUEUE01"; public static void main(String[] args) { // 创建rabbitmq连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); // 设置连接的服务器Host connectionFactory.setHost("localhost"); // 创建连接和信道 因为 Connection 和 Channel 都实现了 java.io.Closeable。这样,我们就不需要在代码中显式关闭它们。 try (Connection connection = connectionFactory.newConnection(); Channel channel1 = connection.createChannel()) { // 发送消息 channel1.queueDeclare(QUEUE_NAME, false, false, false, null); // 发送消息 String message = LocalDateTime.now() + " hello world"; for (int i = 0; i < 100; i++) { channel1.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println("[x] sent" + message); Thread.sleep(1000); } } catch (TimeoutException | IOException | InterruptedException e) { e.printStackTrace(); } } }
channel.queueDeclare(QUEUE_NAME, durable, exclusive, autoDelete, arguments); 参数解析
@ param String QUEUE_NAME 队列的名称; @ param boolean durable 是否持久化;false 表示队列保存到内存中,重启会丢失。 true 表示持久化,重启不会丢失, RabbitMQ 退出时会将队列信息保存到 Erlang 自带的 Mnesia 数据库中。重启 RabbitMQ 会读取数据库。 @ param boolean exclusive 是否排外的;true 表示 仅对首次声明它的连接(Connection)可见,类似加锁,并在连接断开时自动删除。 @ param boolean autoDelete 是否自动删除;true 表示所有消费者都与这个队列断开连接时,这个队列自动删除。 @ param Map arguments 设置队列的其他一些参数,如 x-rnessage-ttl 、x-expires 、x-rnax-length 、x-rnax-length-bytes、x-dead-letter-exchange、 x-deadletter-routing-key 、 x-rnax-priority 等。
- 创建消息消费者Recv.class:
public class Recv { public static final String QUEUE_NAME = "QUEUE01"; public static void main(String[] args) throws Exception { ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("localhost"); Connection connection = connectionFactory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false, false, null); DeliverCallback deliverCallback = (consumerTag, deliver) -> { String message = new String(deliver.getBody(), StandardCharsets.UTF_8); System.out.println(" [X] 接受到 msg : " + message); }; channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {}); } }
总结 : 一对一的模型就是简单的一个生产者和一个消费者,只用到一个消息队列。
2. 工作队列模型
工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务,并且必须等待它完成。相反,我们将任务安排在以后完成。我们将任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作线程时,任务将在它们之间共享。
这个概念在Web应用程序中特别有用,因为在很短的HTTP请求窗口中不可能处理复杂的任务。
代码实现:
在第一个案例的基础上再加入一个消息消费者,并修改消息生产者发送的消息内容:
// Send.class 发送内容修改为以下: for (int i = 0; i < 10; i++) { String message = "[ " + i + " ]" + " hello world"; channel1.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println("[ " + i + " ] send: " + message); Thread.sleep(100); }
开启消费者1Recv.class、消费者2Recv2.class 来接收消息,然后启动生产者Send.class 来发送消息。
结果如下:
总结: 工作队列模型同样也只用到了一个队列,多个消费者共同消费同队列的消息
默认是消息模式是自动确认的,每个消费者轮询来消费消息,消费慢的服务会影响整个消息队列的进程,可以通过修改确认方式为手动,开启能者多劳的模式,消费快的服务可以处理更多的消息。
3. 发布 / 订阅模型
发布/订阅模式和Work模式的区别是:Work模式只存在一个队列,多个消费者共同消费一个队列中的消息;而发布订阅模式存在多个队列,不同的消费者可以从各自的队列中处理完全相同的消息。
RabbitMQ 中消息发布 / 订阅模型的核心思想是生产者从不将任何消息直接发送到队列。实际上很多时候,生产者甚至根本不知道消息是否会传递到任何队列。
相反,创建者只能将消息发送到交换机。交换机一方面,接收来自生产者的消息,另一方面,它将他们推到队列中。交换机必须确切地知道如何处理它收到的消息。是否应将其追加到特定队列?是否应将其附加到多个队列中?或者它应该被丢弃。其规则由交换类型定义
有几种可用的交换类型:
direct
,topic
,headers
和fanout
。我们将重点介绍最后一个 -fanout
。让我们创建一个这种类型的交换,并将其称为logs:
4. 路由模型
路由模式是在使用交换机的同时,生产者指定路由发送数据,消费者绑定路由接受数据。与发布/订阅模式不同的是,发布/订阅模式只要是绑定了交换机的队列都会收到生产者向交换机推送过来的数据。而路由模式下加了一个路由设置,生产者向交换机发送数据时,会声明发送给交换机下的那个路由,并且只有当消费者的队列绑定了交换机并且声明了路由,才会收到数据。下图取自于官方网站(RabbitMQ)的路由模式的图例
5. 主题模型
topic的意思是主题,topic类型的Exchange会根据通配符对Routing key进行匹配,只要Routing key满足某个通配符的条件,就会被路由到对应的Queue上。通配符的匹配规则如下:
● Routing key必须是一串字符串,每个单词用“.”分隔;
● 符号“#”表示匹配一个或多个单词;
● 符号“*”表示匹配一个单词。
例如:“*.123” 能够匹配到 “abc.123”,但匹配不到 “abc.def.123”;“#.123” 既能够匹配到 “abc.123”,也能匹配到 “abc.def.123”。
6. RPC模型
rpc工作模式: 通过消息队列实现rpc功能, 客户端发送消息到消费队列, 服务端进行消费消息执行程序将结果再发送到回调队列, 供客户端使用. 是一种双向生产消费模式.
既是生产者又是消费者, 如果只有一个queue 则会出现死循环, 此时需要有一个回调队列.
客户端发消息时, 消息属性要设置上reply_to的回调队列, 把消息发送到指定的rpc队列, 通过process_data_events来保持连接并且检查是否有回调,
服务端从rpc队列里来确认客户端是否有发消息, 如果有则进行消费处理执行要做的事, 将处理的结果发送到客户端传来的回调队列里供客户端使用.
7. 发布确认模型
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置 basic.ack 的multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
发布确认的策略
1)单个发布确认
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点就是: 发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
2)批量发布确认
上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量。这种方案仍然是同步的,也一样阻塞消息的发布。缺点: 当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。
3)异步发布确认
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与 发布线程 之间进行消息的传递。
ackCallback 只会告诉你是发送失败的编号,不能给你发送失败的数据。所以只能把所有数据都存一边,删
总结
-
一对一
-
一对多
前两种都只有一个队列,多个消费者共享一个队列中的消息
-
发布/订阅模式
由交换机绑定多个队列,每个消费者消费自己的队列中的消息
-
路由模式
在发布订阅模式的基础上,加入路由键,消息通过键路由到不同的队列
-
主题模式
在路由模式基础上,键中加入通配符
-
RPC模式
可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。