目录:
消息队列核心原理(一):消息中间件与协议介绍
消息队列核心原理(二):Kafka核心原理
消息队列核心原理(三):RabbitMQ核心原理
RabbitMQ核心概念
之前我们说了Kafka的核心概念,其实这里大同小异,相似的地方我就简要提一下,重点是不同的地方。
- Broker
- Producer
- Consumer
- Message
- 生产消费的内容 ,由消息头,消息体,属性配置构成
- Queue
- 用于存放消息的容器,对于RabbitMQ的所有消息,均需要存放在Queue里面
- Channel信道
- 全双工,支持多路复用,可以发布、订阅、接收消息
- 该信道基于TCP连接,但是是虚拟连接的,是复用的TCP的连接
- Connection连接
- 一个封装在Socket层面的连接,理论上一个socket可以有多个Channel
- Exchange交换机
- 生产者将消息发送给交换机,交换机再将消息转发到一个或者多个queue中。交换机有多个,所以与queue的关系是多对多。
- RoutingKey路由键
- 生产者将消息发送给交换机,一般会指定RoutingKey,用来确定路由规则
- 最大长度为255Byte
- Binding
- 通过将交换机和Queue绑定起来,RabbitMQ才会知道怎么分配消息。当然是根据BindingKey去路由,而这个BindingKey是提前指定的。
- Virtual Host Machine
- 用于隔离环境:比如测试环境,生产环境,开发环境。
- 用于隔离业务:比如支付业务,日志业务的隔离
- 要注意的是,每个VM数据都是不互通的。一个VM不能有相同的交换机名称和队列名称。
所以,根据上述名词的解释,可以大致理清一下流程:
生产者产生消息---->指定RoutingKey,封装Socket进行连接,可以有多个信道---->消息交给交换机,交换机通过一定的规则检查RoutingKey,与BindingKey进行匹配----->匹配成功后将消息交给Queue---->消费者进行消费
Exchange交换机
特点:
- 只能转发,不能存储
- 交换机机型有多种,与Queue的关系是多对多
- 交换机类型:Direct Exchange(点对点)、Fanout Exchange(广播)、Topic Exchange(主题型,最常用)、Headers Exchange(基本不用)
交换机类型介绍
- Direct Exchange
- 将一个队列绑定到交换机,要求消息与路由键完全匹配,假设路由键是1234,绑定建一定也是1234,无论是多、少、包含都无法匹配。
- Fanout Exchange
- 将队列绑定到交换机,被绑定的队列都会收到消息。消息处理最快(因为不需要check路由键)
- Topic Exchange
- 结合前两个交换机特点,更灵活。对路由键进行匹配。
- Header Exchange
- 略
交换机典型使用场景
我们在处理日志的时候,常常会有ERROR,WARNING这种分类,我们可以把它们作为RoutingKey。这样对于每一条消息,我们便可以分类日志。
消息可靠性保证
RabbitMQ消息传递路径
- 生产者–>交换机–>队列–>消费者
- 通过两点确定消息的可靠性传递
- 生产者到交换机:利用confirmCallback
- 交换机到队列:利用returnCallback
- 消息确认机制耗时,降低性能,因此不能在所有的消息上使用。在一些重要的场合使用即可。比如支付业务。
事务机制
RabbitMQ提供了事务机制,实际上这个事务机制是继承了AMQP的。就是在接收方确认收到消息的情况下再进行下一轮消息传递。
在这种情况下,消息传递后,发送方会被阻塞。源码是这样的:
try {
channel.txSelect();
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
int result = 1 / 0;
channel.txCommit();
} catch (Exception e) {
e.printStackTrace();
channel.txRollback();
}
他会经过4个步骤:
- 打开事务。txSelect()
- 发送音讯。basicPublic()。这个就像你去人家家里做客,提前打电话问人家在不在家,并且简历连接。这个连接一旦因为网络或者其他原因导致了故障,那么就会抛出异常,被捕获后执行第4步,否则执行第三步。
- 提交消息
- 回滚
当我们仔细看源码的话,就会知道这个channel.txSelect()
和channel.txCommit()
都是阻塞的,不收到对面的信息都不会往下执行。
因此,通过描述我们也发现了,这种方式实在是太降低效率了,我们在生产环境中是不用的。
此外,要说明的是,这种方法不能确认消息只发送一次。比如说在消息发送后,但是收到回复消息之前,Channel断掉了,会进行重新发送。
发送方确认机制
这里的发送方确认机制类似于我们TCP的超时重传机制。每发送一定数量的消息,就要接收对面传来的确认帧,从而判要不要重新发送消息。
这里有三种方法:串行模式、批量模式、异步模式。
- 串行模式:每次发送一条消息后等待对面回应。效率一般,但是稳定。
- 批量模式:一次发送多条消息,等待对面回应一条。效率较高,但是不稳定。比如第一条消息就发送失败,这时候发送方还会接着发消息,直到没有收到回应。从第一条消息重新发。
- 异步模式:消息隔一段时间确认一次,也就是说消息的确认可能是逐条,也可能是批量。但是此方法需要自己实现确认接收和确认丢失后的逻辑。即确认接收后你想怎么处理,确认丢失后你又想怎么处理。这里由于逻辑的不确定,就不画图了。
这三种方法,逐条确认最慢,剩下两种差不多(理论上异步更快,实际上时间差的不是很多)。但是当第二种批量确认的方法遇见多条消息丢失的情况,可能反而要慢。比如每批消息都有一条消息丢失,那么这批消息就要重新发送。
幂等性要求
详见上一章:Kafka核心原理。
其实幂等性解决方法都类似,创建唯一key,写进redis。
当然还要看具体业务也可以用其他方法。
TTL与死信队列
TTL(time to live)指的是消息存活时间。当TTL过期的时候,队列中的消息会被清除。
RabbitMQ支持两种TTL设置方式:
- 单消息配置TTL
- 整个队列配置TTL
死信队列:没有被及时消费的消息存放的队列。
死信交换机:Dead Letter Exchange(DLE)当消息成为死信后,会被发到另一个交换机,这个交换机叫做死信交换机。
一般流程:生产者–>交换机–>队列–>消息过期–>死信交换机–>死信队列
成为死信队列的条件
- 消费者拒收消息,并且没有重新入队(requeue = false)
- 消费者在队列中未被消费,且超过队列或消息的本身过期时间
- 队列的消息长度达到极限
延迟队列
生产者在生产消息后,不希望新消息直接投递,而是等待某一个时间节点到来后投递,就是延迟队列。常用于推送定时任务,消费期限设置等。
RabbitMQ不支持延迟队列,所以我们需要用逻辑来替代。
RabbitMQ高可用
说来也怪,我们在讲Redis高可用的时候就是集群,但是RabbitMQ有两种集群模式,一种是普通集群,一种是镜像集群。使用场景、原理都是有差别的。
普通集群
对交换机、队列和虚拟主机的元数据进行同步。
啥意思呢?就是假设有三个节点node1, node2, node3,这三个节点的交换机一模一样,队列一模一样,虚拟主机一模一样。但是就是消息不一样。也就是说消息并不在节点间进行同步。
假设生产者把消息扔给了node1,那么如果消费者连接了node1,可以直接取消息。但是消费者连接了node2,那么node2会从node1拉取数据。也就是说消费者如果连接node2,并不能直接获取数据。这样会出现什么问题?
- 假设node1崩溃了,那么消息不久获取不到了么?对的,除非node1开启了持久化操作,等待node1修复才可以拿到消息,但是如果node1没有开启持久化而且崩溃了,那就真的找不到消息了。
- 链路变长了,假设node1一下来了10w条消息,但是消费者在node2和node3这边,这样的话node1成了性能的瓶颈。
要这种模式干啥啊?又不是真正的高可用,好像没有用。但是人家存在必然有道理的。
分布式日志系统,我们可以利用这种普通集群。首先,日志没有如同支付业务一般重要;其次,多节点可能会导致日志持久化,即数据库连接压力过大。因此,这种简单的集群系统,只需要将node1节点连接数据库,其他节点的日志也会通过node1写入数据库。
镜像集群
除了普通集群同步的数据外,还将消息进行同步。适用于消息过多的情形。
这种镜像集群的方式更像Redis的集群模式,节点分为master节点和slave节点。几乎所有的操作都是通过master节点进行的。我加粗的目的是告诉大家,并不是所有操作都只和master交互,我们一会说这一点。
请看文章:https://blog.csdn.net/weixin_40816738/article/details/105704335(主要看人家的图,镜像队列的结构,太麻烦我懒得画了)
图有点乱,但是根据序号捋一遍就知道大概流程,像这种消息的发送,是一次性发送给所有的节点,因此不怕在消息发送过程中的主节点挂掉。消费者消费之后,master会广播消息被消费,让slave做出相应的动作。这是一个原子化的操作。
节点加入
新加入的节点是不会被选举成master的,master节点只会在老节点中选举。就好比未满18岁的孩子没有被选举权一样。当然,在加入集群前,新的节点内容会完全清空,之后再进行其他同步操作。
没有真正的高可用
假设三个节点,master挂掉,选举slave1成为新的master。在master恢复前,slave1挂掉,slave2成为master。在二者恢复后,数据同步前,slave2挂掉,数据就找不回来了。
假设三个节点,slave相继挂掉后恢复,同步数据前master挂掉,同理,数据也找不回来的。