RabbitMQ消息通信原理


前言

RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。


一、RabbitMQ 消息通信的过程

这张图是大致展示了 RabbitMQ 消息通信的过程

二、概念

1.生产者和消费者

在 RabbitMQ 的通信过程中,有两个主要的角色:生产者和消费者。类比于邮件通信的发送方和接收方。
  这里首先我们要明确 RabbtiMQ 服务器是不能够产生数据的,正如同其名字——消息中间件,是一个用来传递消息的中间商。生产者产生创建消息,然后发布到代理服务器(RabbitMQ),而消费者则从代理服务器获取消息(不是直接找生产者要消息),而且在实际应用中,生产者和消费者也是可以角色互相转换的,所以当我们应用程序连接到 RabbitMQ 服务器时,必须要明确我是生产者呢还是消费者。
  消费者有两种方式从特定的队列中获取消息
  1:通过订阅队列,当消费者消费完或者拒绝后,就会自动获取下一条消息;
  2:应用程序主动一次次获取消息,会影响性能

2.消息

生产者创建消息,然后发布到 RabbitMQ 服务器中,那么什么是消息?
这里的消息分为两部分:有效内容和内容标签。
 ①、有效内容:可以是任何内容,一个数组,一个集合,甚至二进制数据都可以。RabbitMQ 不会在意你发什么数据,尽管发就行了。
②、内容标签:描述有效内容,是 RabbitMQ 用来决定谁将获得消息。前面说的邮件通信,必须明确指定发送方地址和收件方地址,而基于 AMQP 协议的 RabbitMQ 则是通过生产者发送消息附带的内容标签将消息发送个感兴趣的消费者。
在这里插入图片描述

这里只需要知道生产者会创建消息并设置标签。注意最上面的大图,一般来说生产者创建消息会设置标签,但是传输到消费者那里就没有标签了,除非你在有效内容中说明谁是生产者,一般消费者是不知道谁产生的消息的。

3.信道

生产者产生了消息,然后发布到 RabbitMQ 服务器,发布之前肯定要先连接上服务器,也就是要在应用程序和rabbitmq 服务器之间建立一条 TCP 连接,一旦连接建立,应用程序就可以创建一条 AMQP 信道。
  信道是建立在“真实的”TCP 连接内的虚拟连接,AMQP 命令都是通过信道发送出去的,每条信道都会被指派一个唯一的ID(AMQP库会帮你记住ID的),不论是发布消息、订阅队列或者接收消息,这些动作都是通过信道来完成的。
在这里插入图片描述
可能有人会问,为什么不直接通过 TCP 连接来发送AMQP命令呢?
  这里原因是性能和效率问题,因为对于操作系统来说,每次建立和销毁 TCP 会话是非常昂贵的开销,而实际系统中,比如电商双十一,每秒钟高峰期成千上万条连接,一般来说操作系统建立TCP连接是有数量限制的,那么这就会遇到瓶颈。
  引入信道的概念,我们可以在一条 TCP 连接上创建 N多个信道,这样既能发送命令,也能够保证每条信道的私密性,我们可以将其想象为光纤电缆。
  在这里插入图片描述

4.交换器和队列

在这里插入图片描述
 交换器和队列都是 RabbitMQ 服务器的一部分,我们知道生产者会将消息发送到 RabbitMQ 服务器,会在信道里完成匹配的规则而进入该服务器后,然后由信道与对于的队列建立连接,将消息发送到队列
 我们首先来看什么是队列:
①、容纳消息的场所,生产者发送到RabbitMQ服务器的消息会在队列中等待消费者消费。
②、队列是 RabbitMQ 服务器中最后的终点(除非消息进入了死信)。
③、队列可以实现负载均衡,我们可以增加一堆消费者,然后让 RabbitMQ 以循环的方式来均匀的分配消息。
  搞清楚了队列是什么了,那么消息是如何到达队列的呢?没错,就是通过交换器。
消息进入RabbitMQ 服务器时,会首先将消息发送到交换器,然后交换器会根据特定的路由算法以及消息的内容标签将消息绑定到相应的队列。在 AMQP 协议中有四种交换器:direct、fanout、topic和 headers,每种交换器都实现了不同的路由算法,这也对应 RabbitMQ 工作的几种不同方式

4.虚拟主机

在 RabbitMQ 服务器中存在着多个虚拟主机,那么虚拟主机到底是什么?
  首先我们抛出这样一个问题,一个 RabbitMQ 肯定不是只服务一个应用程序,那么多个应用程序同时使用 RabbitMQ 服务器,如何保证彼此之间不会冲突?
  答案就是使用虚拟主机,虚拟主机其实就是一个迷你版的RabbitMQ 服务器,它拥有自己的交换器和队列,更重要的是虚拟主机拥有自己的权限机制,一个服务器能够创建多个虚拟主机。那么我们在使用RabbitMQ服务器的时候,只需要将一个应用程序对应一个虚拟主机,这种各个实例间逻辑上的分离就能够保证不同的应用程序安全的传递消息。
默认的虚拟主机是“/”。

三、消息持久化

1、消息持久化需要满足以下三个条件

  • 把消息的投递模式设置成2(持久)
  • 发送到持久化的交换机
  • 发送到持久化的队列

2、消息持久化过程

消息持久化是将消息写入到磁盘,当生产者发送一条持久化消息到持久化队列上,rabbit会将消息写入到持久化日志文件中后才发送响应,一旦消费者冲队列中消费一条持久化消息,并且确认了,那么rabbit会在持久化日志中将这条消息标记为等待垃圾回收。将消息持久化到SSD上,极大的提高消息的通信性能

四、事务模式和发送方确认模式

当发布操作不返回任何消息给生产者,那么生产者是无法知道消息是否已经持久化到磁盘上。
AMQP事务:当发送一条消息,之后还有许多命令,那么把这些操作都放在一个事务中。事务非常影响性能,出现了更好的方案

try{
	//将channel设置为事务模式
    channel.txSelect();
    //发布消息到交换器,routingKey为空
    channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
    //提交事务,只有消息成功被Broker接收了才能提交成功
    channel.txCommit();
}catch(Exception e) {
	//事务回滚
    channel.txRollback();
}

发送方确认模式:首先先将信道设置成confirm模式,所有在信道上发布的消息都有一个唯一的ID,当消息投递到对应的队列后,信道会发送确认给生产者,包含消息的ID.如果消息是持久化消息的话,那么信道会等待消息持久化到磁盘中才会发送确认。
发送方确认模式最大的好处就是可以是异步等待确认,当生产者发送一条消息后,生产者在等待确认的同时可以继续发送下一条消息,当收到确认消息后,生产者应用的回调方法就是触发来确认。当rabbit内部发生错误,那么会发送一条nack(not acjknowledged 未确认)消息给生产者应用程序。使用发送者确认模式性能比事务模式提高很多

五、消费方确认模式

消费者确认消息,通过ack,可以主动确认或者自动确认,会消息就会从队列中移除,
如果消费者接受消息后不确认消息,那么rabbit主机不会向此消费者发送消息,它认为此消费者没有消费完消息
如果消费者想要拒绝接受消息,前提是在消费者接受到消息后,还没有确认之前,那么是可以主动拒收消息的。通过设置reject,其中有一个参试requeue,如果设置成true,rabbit会将此条消息发送给其他的消费者,如果设置成false,rabbit会将此消息从队列中移除(进入死信队列)。当处理某个消息时碰到此消息格式错误,那么可以通过拒收并且将其从队列中移除。

六:死信队列

1、介绍

死信,顾名思义,就是死掉的消息,死掉的消息是会被一般的队列丢弃的。如果这些消息很重要,而我们又需要,怎么办?凡事都有一个退路,现在就有一种方法可将这些死信消息存下来,那就是DLX(Dead Letter Exchanges)。DLX是专门用来存储死信消息到指定队列中的一种交换机。需要在声明队列时指定DLX和死信存放队列的路由Key,因为RabbitMQ是通过Exchange去匹配路由key寻找队列的。

2、消息进入死信队列的条件

  1. 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用
  2. 上面的消息的TTL到了,消息过期了
  3. 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上

在这里插入图片描述

3、死信队列的应用场景

  • 订单的超时取消
  • 实现定时任务

七:集群

1、四种类型的元数据

  1. 队列元数据—队列名称以及它们的属性(是否可持久化、是否自动删除)
  2. 交换机元数据—交换机名称、类型和属性
  3. 绑定元数据----一张简单的表展示了如何将消息路由到队列
  4. vhost元数据----为vhost内的队列、交换机和绑定提供命名空间和安全属性

2、集群中的队列

在单一节点中,所有关于队列的信息都完全存储在改节点上,但是在集群中,只有队列所有者节点才知道有关队列的元数据信息,其它节点只知道队列的元数据和指向该队列存在的那个节点的地址指针,当该节点崩溃时,所有与该队列的绑定就消失了。
仅当队列最开始并没有设置成持久化队列,消费者才可以重新连接到集群其它节点并创建新得队列,如果队列在创建时设置成持久化,那么消费者是无法重新创建新得队列的,这样才能确保当崩溃的节点恢复时,改节点的队列消息不会丢失
问题: 为什么默认情况下,不将每个队列的元数据信息复制到所有队列上呢?
存储空间:如果每个集群节点都有所有队列的完整拷贝,那么添加新的节点并不会带来更多的存储空间,例如:当一个节点可以存储1GB的消息,那么添加两个节点,每个节点都会存储一摸一样的1GB消息的拷贝
性能:如果将每个消费者发送的每条消息复制到集群的每个节点上,对于持久化的消息来说,每个消息都会产生一次磁盘的读写,这样是非常影响性能的

3:集群中的交换机

队列是有自己的进程的(对应的就是Erlang地址),交换机其实就是一个存放了路由模式的列表和匹配消息应发往对应的队列进程ID的列表。当发布一条消息后,会在信道里完成匹配的规则,然后由信道与对于的队列建立连接,将消息发送到队列。正是因为交换机只是一张查询表,所有在集群中,复制交换机也是非常方便的。将交换机复制到每个节点的好处就是,每个节点上的信道都知道这个交换机,也就能将消息发送到对于的队列节点(生产者可以连接每个节点发送消息)

八:内存节点和磁盘节点

内存节点和磁盘节点的区别就是,内存节点将队列、交换机、绑定、用户、权限、vhost的元数据都是存放在内存中,而磁盘节点则存放在磁盘中。在单节点系统中只允许磁盘类型的节点,在集群中可以选择指定。如果都用磁盘节点,那么队列的声明、交换机的创建等操作都是需要写入到磁盘的,那么像RPC这样的应答队列,每次发送消息都是创建一个临时队列,这样频繁的IO操作,对性能影响很严重。
问题: 在集群中,如何控制内存节点和磁盘节点的数量?
首先我们要知道,如果集群中只有一个磁盘节点,而这个磁盘节点正好崩溃,那么集群中还是可以正常的路由消息,但是是不能创建队列、创建交换机、创建绑定、添加用户、更改权限、添加节点。所以一般在集群中最少设置两台磁盘节点。内存节点在启动时,首先会找到预先配置的磁盘节点(在创建内存节点时,我们应该告诉它所有的磁盘节点信息,因为内存节点唯一存储到磁盘的元数据就是对于的磁盘节点的地址),若找不到则无法重启。MQ集群节点的模式也是有讲究的,一般三个节点会有一个RAM,两个DISK

九:镜像队列

队列默认只会存在其所属的节点上,这样队列就不支持高可用,那么rabbit2.6版本支持镜像队列。镜像队列会存在主节点上,集群中其他节点都会保存改节点的从拷贝。一旦主节点不可用,那么最老的从队列将会被选为主队列。这样保证了队列的高可用,
在普通队列中,信道将消息路由到合适的队列,那么在镜像队列中,信道会将消息投递到主队列和所有的从队列,如果使用发送发确认模式,那么信道将消息投递到镜像队列所有的主从拷贝上后才会反馈。
在镜像队列中如果某个slave失效了,系统处理做些记录外几乎啥都不做:master依旧是master,客户端不需要采取任何行动,或者被通知slave失效。如果master失效了,那么slave中的一个必须被选中为master。被选中作为新的master的slave通常是最老的那个,因为最老的slave与前任master之间的同步状态应该是最好的。然而,特殊情况下,如果存在没有任何一个slave与master完全同步的情况,那么前任master中未被同步的消息将会丢失。
镜像队列不是负载均衡,镜像队列无法提升消息的传输效率,或者更进一步说,由于镜像队列会在不同节点之间进行同步,会消耗消息的传输效率。对exclusive(临时)队列设置镜像并不会有任何作用,因为exclusive队列是连接独占的,当连接断开,队列自动删除。

十:影响消息投递速度

  1. 持久化:在实际场景中,我们会针对不同的消息进行设置,消息的持久化和非持久化对性能的影响也是很大的,想日志消息就可以不设置持久化
  2. 消息确认:消息如果设置成不确认的话,那么服务器将消息发送给消费者就会将消息自动移除队列
  3. 路由算法以及绑定规则:在服务器端,交换机和绑定作为记录存储在Mnesia中,不同的交换机对应的绑定规则不一样,所以消息路由时需要匹配的规则也是不一样的direct和fanout交换机,这两个交换机的查询路由规则不同的在于,fanout交换机在查询rabbit_route表时会忽略路由键,即使你在队列绑定到fanout交换机时和发布消息到fanout交换机时指定了路由键,那么在真正匹配时也会忽略改路由键的
  4. topic交换机:topic交换机的路由规则较为复杂,所以在绑定时会占用更多的内存

十一:消息投递过程

mandatory和immediate是AMQP协议中basic.pulish方法中的两个标志位,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。具体区别在于:

  1. mandatory标志位
    当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者;当mandatory设为false时,出现上述情形broker会直接将消息扔掉。
  2. immediate标志位
    当immediate标志位设置为true时,如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。

在这里插入图片描述


总结

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值