1 RabbitMQ原理
RabbitMQ
是一个由Erlang
语言开发的AMQP
的开源实现
AMQP
:Advanced Message Queue Protocol
,高级消息队列协议,它是应用层协议的一个开放标准
1.1 RabbitMQ原理图
1.2 原理讲解
1.2.1 Message消息
消息是不具名的, 它由消息头消息体组成。消息体是不透明的,而消息头则由一系列可选属性组成,这些属性包括:routing-key
(路由键)、priority
(相对于其他消息的优先权)、 delivery-mode
(指出消息可能持久性存储)等。
1.2.2 Publisher消息生产者
消息生产者,是一个向交换器发布消息的客户端应用程序。
1.2.3 Consumer消息消费者
表示一个从消息队列中取得消息的客户端应用程序。
1.2.4 Exchange交换器
用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
三种常用的交换器类型:
direct
(发布与订阅 完全匹配)fanout
(广播)topic
(主题, 规则匹配)
1.2.5 Binding绑定
用于消息队列
和交换器
之间的关联。 一个绑定就是基于路由键
将交换器和消息队列连接起来的路由规则, 所以可以将交换器
理解成一个由绑定
构成的路由表
。
1.2.6 Queue消息队列
用来保存消息直到发送给消费者。 它是消息的容器
, 也是消息的终点
。 一个消息可投入一个或多个队列。 消息一直在队列里面, 等待消费者链接到这个队列将其取走。
1.2.7 Routing-key路由键
RabbitMQ
决定消息该投递到哪个队列的规则。队列通过路由键绑定到交换器。
消息发送到MQ
服务器时,消息将拥有一个路由键, 即便是空的,RabbitMQ
也会将其和绑定使用的路由键进行匹配。
如果相匹配, 消息将会投递到该队列。
如果不匹配, 消息将会进入黑洞。
1.2.8 Connection链接
指rabbit
服务器和服务建立的TCP
链接。
1.2.9 Channel信道
Channel
中文叫做信道, 是TCP
里面的虚拟链接。 例如: 电缆相当于TCP
, 信道是一个独立光纤束, 一条TCP
连接上创建多条信道是没有问题的。TCP
一旦打开, 就会创建AMQP
信道。- 无论是
发布消息
、接收消息
、订阅队列
, 这些动作都是通过信道完成的。
1.2.10 Virtual Host虚拟主机
表示一批交换器
, 消息队列
和相关对象
。
虚拟主机是共享相同的身份认证
和加密环境
的独立服务器域。 每个vhost
本质上就是一个mini
版的RabbitMQ
服务器,拥有自己的队列、 交换器、 绑定和权限机制。
vhost
是AMQP
概念的基础, 必须在链接时指定,RabbitMQ
默认的vhost
是/
1.2.11 Borker服务器实体
表示消息队列服务器实体
。
1.2.12 交换器和队列的关系
交换器
是通过路由键
和队列
绑定在一起的, 如果消息拥有的路由键跟队列和交换器的
路由键匹配, 那么消息就会被路由到该绑定的队列中。
也就是说, 消息到队列的过程中, 消息首先会经过交换器, 接下来交换器在通过路由
键匹配分发消息到具体的队列中。
路由键可以理解为匹配的规则。
1.2.13 RabbitMQ为什么需要信道,为什么不是TCP直接通信
TCP
的创建和销毁开销特别大。 创建需要3
次握手, 销毁需要4
次挥手。- 如果不用信道, 那应用程序就会以
TCP
链接Rabbit
, 高峰时每秒成千上万条链接会造成资源巨大的浪费,而且操作系统每秒处理TCP
链接数也是有限制的, 必定造成性能瓶颈。 - 信道的原理是一条线程一条通道,多条线程多条通道同用一条
TCP
链接。一条TCP
链接可以容纳无限的信道, 即使每秒成千上万的请求也不会成为性能的瓶颈。
1.3 RabbitMQ中的消息确认ACK机制
1.3.1 消息确认ACK理解
如果在处理消息的过程中,消费者的服务器在处理消息时出现异常,那可能这条正在处理的消息就没有完成消息消费,数据就会丢失
,为了确保数据不丢失,RabbitMQ
支持消息确认-ACK
1.3.2 ACK的消息确认机制
ACK
机制是消费者从RabbitMQ
收到消息并处理完成后,反馈给RabbitMQ
,RabbitMQ收到反馈后才将此消息从队列中删除
- 如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就
不会有ACK反馈,
RabbitMQ会认为这个消息没有正常消费,
会将消息重新放入队列中` - 如果在集群情况下,
RabbitMQ
会立即将这个消息推送给这个在线的其他消费者。这种机制保证了在消费者服务端故障的时候,不丢失任何消息和任务 消息永远不会从RabbitMQ中删除
:只有当消费者正确发送ACK
反馈,RabbitMQ
确认收到后,消息才会从RabbitMQ
服务器的数据中删除- 消息的
ACK
确认机制默认是打开的
1.3.3 ACK机制的注意
如果忘记了ACK
,那么后果很严重。当consumer
退出时,Message
会一直重新分发。
然后RabbitMQ
会占用越来越多的内存
,由于RabbitMQ
会长时间运行,因此内存泄漏
是致命的
1.3.4 接受方出现异常
因为接受方consumer
出现异常,消息会放到队列里面,而导致这个消息不会删除而一直都是那个消息重复发送,为了预防网络波动的异常,而不得不使用原来的消息,就应该开启重试机制,而应该使用如下配置。
#开启重试
spring.rabbitmq.listener.simple.retry.enabled=true
#重试次数, 默认为 3 次
spring.rabbitmq.listener.simple.retry.max-attempts=3
1.4 RabbitMQ特性
1.4.1 顺序性
当 RabbitMQ
把消息放入到对应的队列后,我们紧接着面临的问题就是,我们应该在系统内部启动多少线程去从消息队列中获取消息。
如果只是单线程去获取消息,那自然没有什么好说的。但是多线程情况,可能就会有问题了……
RabbitMQ
有这么个特性,它在官方文档就声明了自己是不保证多线程消费同一个队列的消息,一定保证顺序的。而不保证的原因,是因为多线程时,当一个线程消费消息报错的时候,RabbitMQ
会把消费失败的消息再入队,此时就可能出现乱序的情况。
1.4.2 消息延迟性
RabbitMQ
的消息自带手表,消息中有个 TTL
字段,可以设置消息在 RabbitMQ
中的存放的时间,超时了会被移送到一个叫死信队列的地方。
所以,延迟队列 RabbitMQ
最简单的实现方式就是设置 TTL
,然后一个消费者去监听死信队列
。当消息超时了,监听死信队列的消费者就收到消息了。
不过,这样做有个大问题:假设,我们先往队列放入一条过期时间是 10 秒的 A 消息,再放入一条过期时间是 5 秒的 B 消息。那么问题来了,B 消息会先于 A 消息进入死信队列吗?
答案是否定的。
B 消息会优先遵守队列的先进先出规则
,在 A 消息过期后,和其一起进入死信队列被消费者消费。
在 RabbitMQ
的 3.5.8
版本以后,官方推荐的 rabbitmq delayed message exchange
插件可以解决这个问题。用了这个插件,我们在发送消息的时候,把消息发往一个特殊的 Exchange
。同时,在消息头里指定要延迟的时间。收到消息的 Exchange
并不会立即把消息放到队列里,而是在消息延迟时间到达后,才会把消息放入。
1.4.3 消息保持
在微服务里,事件溯源模式是经常用到的。如果想用消息队列实现,一般是把事件当成消息,依次发送到消息队列中。
事件溯源有个最经典的场景,就是事件重放
。简单来讲就是把系统中某段时间发生的事件依次取出来再处理。而且,根据业务场景不同,这些事件重放很可能不是一次,更可能是重复 N 次。
假设,我们现在需要一批在线事件重放,去排查一些问题。
RabbitMQ
此时就真的不行了,因为消息被人取出来就被删除了,想再次被重复消费,可能做不到。
但是可以使用 Kafka
,消息会被持久化一个专门的日志文件里,不会因为被消费了就被删除
1.4.4 消息错误处理
RabbitMQ
由于会在消息出问题或者消费错误的时候,可以重新入队
或者移动消息
到死信队列,继续消费后面的,会省心很多
1.4.5 消息的吞吐量
Kafka
是每秒几十万条消息吞吐,RabbitMQ
的吞吐量是每秒几万条消息