RabbitMQ详解

前言

​ **消息队列中间件 (Message Queue Middleware,简称 MQ) 是指利用高效可靠的消息传递机制进行与平台无关的数据交流,它可以在分布式环境下扩展进程间的数据通信,并基于数据通信来进行分布式系统的集成。**它主要适用于以下场景:

  • 项目解耦:不同的项目或模块可以使用消息中间件进行数据的传递,从而可以保证模块的相对独立性,实现解耦。
  • 流量削峰:可以将突发的流量 (如秒杀数据) 写入消息中间件,然后由多个消费者进行异步处理。
  • 弹性伸缩:可以通过对消息中间件进行横向扩展来提高系统的处理能力和吞吐量。
  • 发布订阅:可以用于任意的发布订阅模式中。
  • 异步处理:当我们不需要对数据进行立即处理,或者不关心数据的处理结果时,可以使用中间件进行异步处理。
  • 冗余存储:消息中间件可以对数据进行持久化存储,直到你消费完成后再进行删除。

RabbitMQ 简介

RabbitMQ 完全实现了 AMQP 协议,并基于相同的模型架构。RabbitMQ 在实现 AMQP 0-9-1 的基础上还进行了额外拓展,并可以通过插件来支持 AMQP 1.0。所以在某种程度上而言, RabbitMQ 就是 AMQP 在 Erlang 语言上的实现。RabbitMQ 基于众多优秀的特性成为了目前最为广泛使用的消息中间件,它的主要特性如下:

  • 支持多种消息传递协议,除了 AMQP 外,还可以通过插件支持所有版本的 STOMP 协议和 MQTT 3.1 协议;
  • 拥有丰富的交换器类型,可以满足绝大部分的使用需求;
  • 支持多种部署方式,易于部署
  • 支持跨语言开发,如:Java,.NET,PHP,Python,JavaScript,Ruby,Go;
  • 可以通过集群来实现高可用性和高吞吐,还可以通过 Federation 插件来连接跨机房跨区域的不同版本的服务节点
  • 插拔式的身份验证和授权,支持 TLS 和 LDAP
  • 支持持续集成,能够使用各种插件进行灵活地扩展
  • 能够使用多种方式进行监控和管理,如 HTTP API,命令行工具和 UI 界面。

AMQP协议

​ AMQP (Advanced Message Queuing Protocol) 是一个提供统一消息服务的应用层通讯协议,为消息中间件提供统一的开发规范。**不同客户端可以将消息投递到中间件上,或从上面获取消息;发送消息和接收消息的客户端可以采用不同的语言开发、不同的技术实现,但必须遵循相同的 AMQP 协议。**AMQP 协议本身包括以下三层:

  • Module Layer:位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑。例如:可以使用 Queue.Declare 命令声明一个队列或者使用 Basic.Consume 订阅消费一个队列中的消息。
  • Session Layer:位于中间层,主要负责将客户端的命令发送给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性同步机制和错误处理。
  • Transport Layer:位于最底层,主要传输二进制数据流 ,提供帧的处理、信道复用、错误检测和数据表示等。

模型架构

​ RabbitMQ 与 AMQP 遵循相同的模型架构。

1. Publisher(发布者)

发布者 (或称为生产者) 负责生产消息并将其投递到指定的交换器上。

2. Message(消息)

消息由消息头和消息体组成。消息头用于存储与消息相关的元数据:如目标交换器的名字 (exchange_name) 、路由键 (RountingKey) 和其他可选配置 (properties) 信息。消息体为实际需要传递的数据。

3. Exchange(交换器)

交换器负责接收来自生产者的消息,并将将消息路由到一个或者多个队列中,如果路由不到,则返回给生产者或者直接丢弃,这取决于交换器的 mandatory 属性

  • 当 mandatory 为 true 时:如果交换器无法根据自身类型和路由键找到一个符合条件的队列,则会将该消息返回给生产者
  • 当 mandatory 为 false 时:如果交换器无法根据自身类型和路由键找到一个符合条件的队列,则会直接丢弃该消息

4. BindingKey (绑定键)

交换器与队列通过 BindingKey 建立绑定关系

5. Routingkey(路由键)

生产者将消息发给交换器的时候,一般会指定一个 RountingKey,用来指定这个消息的路由规则。当 RountingKey 与 BindingKey 基于交换器类型的规则相匹配时,消息被路由到对应的队列中。

6. Queue(消息队列)

用于存储路由过来的消息。多个消费者可以订阅同一个消息队列,此时队列会将收到的消息将以轮询 (round-robin) 的方式分发给所有消费者。即每条消息只会发送给一个消费者,不会出现一条消息被多个消费者重复消费的情况。

7. Consumer(消费者)

消费者订阅感兴趣的队列,并负责消费存储在队列中的消息。为了保证消息能够从队列可靠地到达消费者,RabbitMQ 提供了消息确认机制 (message acknowledgement),并通过 autoAck 参数来进行控制

  • 当 autoAck 为 true 时:**此时消息发送出去 (写入TCP套接字) 后就认为消费成功,而不管消费者是否真正消费到这些消息。**当 TCP 连接或 channel 因意外而关闭,或者消费者在消费过程之中意外宕机时,对应的消息就丢失。因此这种模式可以提高吞吐量,但会存在数据丢失的风险。
  • 当 autoAck 为 false 时:**需要用户在数据处理完成后进行手动确认,只有用户手动确认完成后,RabbitMQ 才认为这条消息已经被成功处理。**这可以保证数据的可靠性投递,但会降低系统的吞吐量。

8. Connection(连接)

​ 用于传递消息的 TCP 连接。

9. Channel(信道)

​ **RabbitMQ 采用类似 NIO (非阻塞式 IO ) 的设计,通过 Channel 来复用 TCP 连接,并确保每个 Channel 的隔离性,就像是拥有独立的 Connection 连接。**当数据流量不是很大时,采用连接复用技术可以避免创建过多的 TCP 连接而导致昂贵的性能开销。

10. Virtual Host(虚拟主机)

​ **RabbitMQ 通过虚拟主机来实现逻辑分组和资源隔离,一个虚拟主机就是一个小型的 RabbitMQ 服务器,拥有独立的队列、交换器和绑定关系。**用户可以按照不同业务场景建立不同的虚拟主机,虚拟主机之间是完全独立的,你无法将 vhost1 上的交换器与 vhost2 上的队列进行绑定,这可以极大的保证业务之间的隔离性和数据安全。默认的虚拟主机名为 /

11. Broker

​ 一个真实部署运行的 RabbitMQ 服务。

交换器

RabbitMQ 支持多种交换器类型,常用的有以下四种:fanout、direct、topic、headers。

fanout

​ 这是最简单的一种交换器模型,此时会把消息路由到与该交换器绑定的所有队列中。任何发送到 X 交换器上的消息,都会被路由到属于 X 交换器上的 Q1 和 Q2 两个队列上。

direct

任何发送到 X 交换器上的消息,把消息路由到 BindingKey 和 RountingKey 完全一样的队列中。当消息的 RountingKey 为 orange 时,消息会被路由到 Q1 队列;当消息的 RountingKey 为 black 或 green 时,消息会被路由到 Q2 队列。

需要特别说明的是一个交换器绑定多个队列时,它们的 BindingKey 是可以相同的,例如:此时当消息的 RountingKey 为 black 时,消息会同时被路由到 Q1 和 Q2 队列。

topic

将消息路由到 BindingKey 和 RountingKey 相匹配的队列中,匹配规则如下:

  • RountingKey 和 BindingKey 由多个单词使用逗号 . 进行连接;
  • BindingKey 支持两个特殊符号:#* 。其中 * 用于匹配一个单词, # 用于匹配零个或者多个单词。例如:*.orange、orange.#。

RabbitMQ详解_第1张图片

路由键说明:

  • 路由键为 lazy.orange.elephant 的消息会发送给所有队列;
  • 路由键为 quick.orange.fox 的消息只会发送给 Q1 队列;
  • 路由键为 lazy.brown.fox 的消息只会发送给 Q2 队列;
  • 路由键为 lazy.pink.rabbit 的消息只会发送给 Q2 队列;
  • 路由键为 quick.brown.fox 的消息与任何绑定都不匹配;
  • 路由键为 orangequick.orange.male.rabbit 的消息也与任何绑定都不匹配。

headers

​ **在交换器与队列进行绑定时可以指定一组键值对作为 BindingKey;在发送消息的 headers 中的可以指定一组键值对属性,当这些属性与 BindingKey 相匹配时,则将消息路由到该队列。**同时还可以使用 x-match 参数指定匹配模式:

  • x-match = all :所有的键值对都相同才算匹配成功;
  • x-match = any:只要有一个键值对相同就算匹配成功。

​ headers 类型的交换器性能比较差,因此其在实际开发中使用得比较少。

死信队列

​ **RabbitMQ 中另外一个比较常见的概念是死信队列。当消息在一个队列中变成死信 (dead message) 之后,它可以被重新被发送到死信交换器上 (英文为 Dead-Letter-Exchange,简称 DLX ),任何绑定死信交换器的队列都称之为死信队列。**需要特别说明的是死信交换器和死信队列与正常的交换器和队列完全一样,采用同样的方式进行创建,它们的名称表达的是其功能,而不是其类型。

一个正常的消息变成死信一般是由于以下三个原因:

  • 消息被拒绝 (Basic.Reject/Basic.Nack) ,井且设置重回队列的参数 requeue 为 false;
  • 消息过期;
  • 队列达到最大长度。

​ **可以在队列创建的 channel.queueDeclare 方法中设置 x-dead-letter-exchange 参数来为正常队列添加死信交换器,当该队列中存在死信时,死信就会被发送到死信交换器上,进而路由到死信队列上。**示例如下:

// 创建死信交换器
channel.exchangeDeclare("exchange.dlx", "direct");
// 声明死信队列
channel.queueDeclare(" queue.d1x ", true, false, false, null);
// 绑定死信交换器和死信队列
channel.queueBind("queue.dlx ", "exchange.dlx ", "routingkey");

Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "exchange.dlx");
// 为名为 myqueue 的正常队列指定死信交换器
channel.queueDeclare("queue.normal", false, false, false, args);

//除此之外,还可以重新指定死信的路由键,如果没有指定,则默认使用原有的路由键,重新设置的方法如下:
args.put("x-dead-letter-routing-key", "some-routing-key");

消息投递策略

默认情况下RabbitMQ的队列和交换机在RabbitMQ服务器重启之后会消失,原因在于队列和交换机的durable属性,该属性默认情况下为false。能从AMQP服务器崩溃中恢复的消息称为持久化消息,如果想要从崩溃中恢复那么消息必须

  • 投递模式设置2,来标记消息为持久化
  • 发送到持久化的交换机
  • 到到持久化的队列

缺点:

消息写入磁盘性能差很多,没有采用缓冲区刷盘机制,所以除非特别关键的消息会使用。

事务:

​ 对事务的支持是AMQP协议的一个重要特性。假设当生产者将一个持久化消息发送给服务器时,因为consume命令本身没有任何Response返回,所以即使服务器崩溃,没有持久化该消息,生产者也无法获知该消息已经丢失。

如果此时使用事务,即通过txSelect()开启一个事务,然后发送消息给服务器,然后通过txCommit()提交该事务,即可以保证,如果txCommit()提交了,则该消息一定会持久化,如果txCommit()还未提交即服务器崩溃,则该消息不会服务器接收。当然Rabbit MQ也提供了txRollback()命令用于回滚某一个事务。

监听不可达消息

​ 消费者需要监听不可到达消息,并设置默认返回监听。也可以把不可到达的小鞋添加到死信队列,用于后续追踪。

消费端限流

​ 在高并发的时候,瞬间产生的流量很大,消息很大,而MQ有个重要的作用就是限流,限流则是消费端做的。RabbitMQ提供了一种Qos(服务质量保证)功能,即在非自动确认消息的前提下,在一定数量的消息未被消费前,不进行消费新的消息

​ 注意: autoAck设置为false, 一定要手工签收消息

// prefetchSize消息的限制大小,一般设置为0,在生产端限制
// prefetchCount 我们一次最多消费多少条消息,一般设置为1
// global,一般设置为false,在消费端进行限制
channel.basicQos(int prefetchSize, int prefetchCount, boolean global) 

// 使用
channel.basicQos(0, 1, false);
channel.basicConsume(queueName, false, new MyConsumer(channel));    

消息重试

​ rabbitMQ为自带了消息重试机制:当消费者消费消息失败时,可以选择将消息重新“推送”给消费者,直至消息消费成功为止。

//开启自带的重试机制,需要如下几个配置:
//1 开启消费者手动应答机制,对应的springboot配置项:
spring.rabbitmq.listener.simple.acknowledge-mode=manual
//2 消费异常时,设置消息重新入列
 boolean multiple = false; // 单条确认
 boolean requeue  = true; // 重新进入队列,谨慎设置!!!很容易导致死循环,cpu 100%

延迟队列

​ **延迟队列基于rabbitmq_delayed_message_exchange插件,实现延迟队列效果。它是一种新的交换类型,该类型消息支持延迟投递机制消息传递后并不会立即投递到目标队列中,而是存储在mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。**使用延迟队列,可以有效解决定时任务带来的系统压力以及业务处理时效性等问题。

优先级队列

​ 优先级队列,也就是具有高优先级的队列,优先级高的消息具备优先被消费的特权。通过队列的 x-max-priority 参数设置队列的最大优先级,之后在发送消息时通过 priority 属性再设置当前消息的优先级。优先级应在 0 和 255 之间,推荐1 ~ 10。

  • 优先级默认最低为0,最高为队列设置的最大优先级;
  • 对于单条消息来谈优先级是没有什么意义的。假如消费者的消费速度大于生产者的速度且Broker中没有消息堆积的情况下,对发送的消息设置优先级就没有什么意义,因为生产者刚发完一个消息就被消费者消费了,相当于Broker中至多只有一条消息。

惰性队列

惰性队列会尽可能地将消息存入磁盘中,而在消费者消费消息时才会被加载到内存中,它支持更多的消息存储。

队列具备两种模式:default 和 lazy。默认的为 default 模式,在队列声明的时候可以 通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。

RabbitMQ的内存换页

​ 在某个Broker节点及内存阻塞生产者之前,它会尝试将队列中的消息换页到磁盘以释放内存空间,持久化和非持久化的消息都会写入磁盘中,其中持久化的消息本身就在磁盘中有一个副本,所以在转移的过程中持久化的消息会先从内存中清除掉。

默认情况下,内存到达的阈值是50%时就会换页处理。也就是说,在默认情况下该内存的阈值是0.4的情况下,当内存超过0.4*0.5=0.2时,会进行换页动作。

RabbitMQ的磁盘预警

当磁盘的剩余空间低于确定的阈值时,RabbitMQ同样会阻塞生产者,这样可以避免因非持久化的消息持续换页而耗尽磁盘空间导致服务器崩溃。

​ 默认情况下:**磁盘预警为50MB的时候会进行预警。表示当前磁盘空间第50MB的时候会阻塞生产者并且停止内存消息换页到磁盘的过程。**这个阈值可以减小,但是不能完全的消除因磁盘耗尽而导致崩溃的可能性。比如在两次磁盘空间的检查空隙内,第一次检查是:60MB ,第二检查可能就是1MB,就会出现警告。

总结

​ RabbitMQ可以用于分布式系统之间通信。内部采用AMQP协议对流量可以进行很好的肖峰,交换器有多种类型,可以对不同业务场景的消息采用不同的交换器类型。RabbitMQ 提供了消息确认机制,支持消息重试,也支持事务型消息,如果txCommit()提交了,则该消息一定会持久化。但是会存在消息未成功消费,消息丢失情况。

​ 消息的持久化必须是三个环节都进行持久化(投递模式、持久化交换机、持久化队列),而且持久化不是采用通过缓冲区刷盘到磁盘中,所以一旦启动持久化的消息,在性能方面非常差。

你可能感兴趣的:(中间件,rabbitmq)