前言
小组分享
什么是MQ?
全称:Message Queue Middleware
消息队列中间件;
指利用高效可靠的消息传递机制进行与平台无关的数据交流;
通过提供消息传递和消息排队模型,它可以再分布式环境下扩展进程间的通信。
削峰填谷
消息的传递模式
① 点对点模式
② 发布/订阅模式(pub/sub)
RabbitMQ原理
本质上呢,就是一个生产者与消费者模型,主要负责接收、存储和转发消息。
假设将RabbitMQ
整体抽象化:
形象点的理解:
寄快递,快递员上门取件,快递公司会暂存快递到集散点,运送到目的地后,由快递员送到收件人手里。
那么Rabbitmq
就好比由快递公司、集散点和快递员组成的一个系统。
Broker
可以看作是服务器的节点,或者服务实例;一般情况下,我们就是看作rabbitmq的服务器。
队列
是Rabbitmq的内部对象,作用:用来存储消息。
最简单的实现方式;
现在考虑这样一个场景,:
想把包含:rabbitmq
这类消息投递到队列1
中;
想把包含:股票
这类消息的投递到队列2
中;
假设基于上面的简单模型,就很难实现;
所以,实际上生产者是将消息发送到交换器
中,由交换器将消息路由到一个或多个队列中。
如果路由不到,可能返回给消费者,可能丢弃掉。
交换器
交换器可以理解为一个简单的实体;类似中转站;
负责将消息分门别类的发送到相应的队列中。
交换器的类型
类型 | 说明 |
---|---|
fanout | 它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中 |
direct | 将消息路由到路由键 和绑定键 完全匹配的队列中 |
topic | 对direct的扩展;对路由键 支持模糊匹配;* 匹配一个单词,# 匹配多规格单词,可以是0个 |
headers | 该交换器不依赖路由键的匹配规则,而是根据消息内容中headers属性来匹配。基本不用 |
路由
当生产者发消息给交换器时,得告诉交换器这条消息应该去哪(队列)。
那么这个东东就是路由键
(routingkey
).
这里需要注意:虽然生产者告诉了交换器这条消息应该去哪里,但是交换器又是怎么知道应该去哪个队列呢?
这里就需要将队列和交换器进行绑定,有了这层绑定关系,交换机就知道去哪里了;
这层绑定关系,我们称为 绑定键
(bindingkey
)。
查阅大量资料,你会发现,绑定键会习惯性写成路由键。官方的解释为:绑定时的路由键;
所以通常我们会将这两个都统称为路由键
。
信道
Q:为什么不直接使用Connection
来完成信道的工作呢?
A:假设一个应该程序有多个线程需要从RabbitMQ
中消费消息,那么必然需要建立很多个Connection
,也就是很多个TCP
连接。而操作系统建立这种连接是非常昂贵的开销,而RabbitMQ
采用类型NIO
的做法,选择TCP
连接复用,既能减少开销,又能方便管理。
RabbitMQ 如何发送消息
上面我们知道了,从消息生产出来后,会经过交换器
转发给队列
。
类似Java思想:先声明,在使用
① 创建交换器
channel.exchangeDeclare(exchangeName, "direct", true);
② 创建队列
String queueName = channel.queueDeclare();
③ 绑定交换器和队列
channel.queueBind(queueName, exchangeName, routingKey);
这里的
路由键
,我们也可以称为绑定键
;用于决定交换器的消息转发给哪个队列
④ 发送消息
channel.basicPublish(exchangeName, routingKey, BasicProperties, message);
这里的
路由键
,决定消息发送给哪个交换器
RabbitMQ 如何消费消息
消费消息有两种模式:
- 推模式
和发送消息类似,前面三个步骤是一样的,第四步:
channel.basicConsume(queueName, autoAck, callback)
说明:
① 正常情况下,交换器和队列其实会提前创建好,所以消费端,其实不用再声明交换器
和队列
了,因此直接使用即可。
- 拉模式
单条的获取消息。
GetResponse response = channel.basicGet(queue_name, autoAck);
response.getBody();
说明:
① 如果只想从队列中获得单条消息而不是持续订阅,那么建议使用Basic.Get
进行消费。
② 理论上,可以将Basic.Get
放入循环来实现类似推模式,但是这会严重影响性能。
RabbitMQ 如何持久化
RabbitMQ持久化分为三个部分:
① 交换器持久化
② 队列持久化
③ 消息持久化
交换器的持久化:
channel.exchangeDeclare(exchangeName, "direct", true);
第三个参数durable
设置为true
;如果设置为false
,重启后,交换器就丢失了。
队列持久化:
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args)
将durable
设置为true
,因为消息时存放在队列中的,所以队列如果不持久话,消息一定会丢失。
消息持久化:
channel.basicPublish(exchangeName, routingKey, BasicProperties, message);
在发送消息时,利用BasicProperties
将投递模式设置为2
,消息就会被持久化:
new BasicProperties("text/plain", null, null, 2//投递模式, 0, null...);
持久化消息后,就能保证消息不丢失吗?
通过上面我们了解了,持久化的具体操作;
那么如果我们把,交换器、队列和消息都设置为持久化就能百分百保证数据不丢失吗?
A: 不行。
情形一:如果订阅消费队列使用的是autoAck=true
,那么当消费者接收到相关消息后,就宕机了,这其实也算消息丢失;解决办法:设置为false,然后手动确认
情形二:持久化的消息,正确的存入RabbitMQ
之后,并不是里面就同步存入磁盘中,因为这种落盘操作时需要调用操作系统fsync
方法来进行同步落盘,这时消息只是在操作系统的缓存中,如果这段时间RabbitMQ服务节点发生了宕机,重启等异常,消息就会丢失。
解决办法:① 使用RabbitMQ 镜像队列机制,相当于配置了副本,如果主节点挂了,可以自动切换到从节点。
② 在发送端引入事务机制
、发送确认机制
来保证消息确实是落盘了。