本文摘要:
接下来 ,咱们逐一讲解。
1 为什么要选择 RabbitMQ
1、RabbitMQ 是实现了 AMQP 标准的消息服务器;
2、可靠性:RabbitMQ 的持久化支持,保证了消息的稳定性;
3、高并发:RabbitMQ 使用了 Erlang 开发语言,Erlang 是为电话交换机开发的语言,天生自带高并发光环,和高可用特性;
4、集群部署简单,正是应为 Erlang 使得 RabbitMQ 集群部署变的超级简单;
5、社区活跃度高,根据网上资料来看,RabbitMQ 也是首选;
2 使用场景
2.1 应用解耦(异步处理)
上游系统处理完成之后,把数据消息写入消息队列中,业务逻辑完成;下游系统可以订阅消息队列,也可以自由的从消息队列拉取消息。上下游毫不关联。
2.2 同步调用
能够拿到下游系统异步处理的结果。
2.3 顺序调度
先进先出原则去调度有严格先后顺序的任务。
2.4 通知分发
指消息多方分发,例如订单系统的下单消息几乎要分发给所有的其他系统,主要利用 Exchange 的 fanout 类型。
2.5 高并发缓存
我们可以把所有的请求全部存入到队列(队列的存储能力理论上无限的,主要受制于空间),然后再批量取出能处理数量的消息处理(主要利用消息队列作为消息存储容器的特性)。
2.6 并发限流
高并发的情况下,只接受指定数量的请求,队列满了就直接拒绝(主要利用消息队列的设置队列长度特性)。
2.7 延迟任务调度
为消息设置等同延时的有效期,消息过期后进入到死信队列,然后再到死信队列取消息处理。
3 RabbitMQ 工作机制
RabbitMQ 实现了AMQP 0-9-1 标准。作为中间件协议,AMQP(高级消息队列协议)是一个用于在分布式系统中存储转发消息进行通信网络协议。
在上面的使用场景分析中我们可以看出来,消息中间件(brokers)主要承担一个消息(message)容器的角色,它接收从发布者(publishers)亦称生产者(producers)那儿来的消息,并根据既定的路由规则,把接收到的消息发送给处理消息的消费者(consumers)处理——实际上 RabbitMQ 是生产者(producers)投递到交换机(exchange),exchange 按照路由规则分发到特定的队列(queue),再推送给消费者(consumers),或者消费者(consumers)主动拉取。
示意图中所示,消息由 “生产者”(producer / publisher)通过 “消息代理”(broker)传递到 “消费者”(consumer),具体而言:
1、消息由 “生产者” 发布到 “交换器”(exchange);
2、“交换器” 根据 “绑定”(binding),将消息路由(分发)到队列(queue);
3、“消费者” 获取 “队列” 中的消息。
4、AMQP 中,“队列”、“交换器”、“绑定”,被称为 “实体”(entity)。
5、“消息确认” :允许 “消费者” 收到消息时,通知 “消息代理”,此时,消息将被 “消息代理” 从 “队列” 中移除。
4 消息发送原理
你的应用程序和 Rabbit Server 之间会创建一个 TCP 连接,一旦 TCP 打开,并通过了认证(认证就是你试图连接 Rabbit 之前发送的 Rabbit 服务器连接信息和用户名和密码,有点像程序连接数据库),你的应用程序和 Rabbit 就创建了一条 AMQP 信道(Channel)。
信道是创建在 “ 真实 ” TCP 上的虚拟连接,AMQP 命令都是通过信道发送出去的,每个信道都会有一个唯一的 ID,不论是发布消息,订阅队列或者接收消息都是通过信道完成的。即:ConnectionFactory->channel。
4.1 为什么不通过 TCP 直接发送命令?
对于操作系统来说创建和销毁TCP会话是非常昂贵的开销,假设高峰期每秒有成千上万条连接,每个连接都要创建一条 TCP 会话,这就造成了 TCP 连接的巨大浪费,而且操作系统每秒能创建的 TCP 也是有限的,因此很快就会遇到系统瓶颈。
如果我们每个请求都使用一条 TCP 连接,既满足了性能的需要,又能确保每个连接的私密性,这就是引入信道概念的原因。
5 基本概念
5.1 AMQP 协议层角色相关的概念
5.1.1 生产者
产生消息的应用,能够传递消息到消息中间件的应用。
5.1.2 消息中间件(brokers)
消息传递的中间载体,即我们今天的主角 RabbitMQ。
5.1.3 消费者
接收并处理消息的应用。从消息中间件获取消息并处理。
5.1.4 连接(Connection)
1、应用程序与 Rabbit 之间建立连接的管理器,程序代码中使用。
2、主要表示生产者/消费者与消息代理(Rabbitmq)建立的 TCP 连接,AMQP 的连接通常是长连接,AMQP 支持鉴权和 TLS,以确保 “连接” 的数据安全。
3、当我们的生产者或消费者不再需要连接到消息中间件的的时候,需要优雅的释放掉它们与消息中间件 TCP 连接,而不是直接将 TCP 连接关闭。
5.1.5 信道(Channel)
1、消息推送使用的通道,可以把通道理解成共享一个 TCP 连接的多个轻量化连接。
2、一个特定通道上的通讯与其他通道上的通讯是完全隔离的,因此每个 AMQP 方法都需要携带一个通道号。
3、AMQP 通过 “信道”(Channel)构建 “连接” 的多路复用:
1)“信道” 共享 “连接”。
2)“信道” 相互独立。
3)任何 AMQP 通信,都属于 “信道” 层面的通信。
5.2 消息中间件相关的概念
5.2.1 虚拟主机(Virtual Host)
1、每个 Rabbit 都能创建很多 vhost,我们称之为虚拟主机,每个虚拟主机其实都是 mini 版的 RabbitMQ,拥有自己的队列,交换器和绑定,拥有自己的权限机制。
2、AMQP 以 “虚拟主机”(virtual host)形式,于消息代理中提供 “隔离” 的运行环境。默认 “虚拟主机”:/。
3、多个 vhost 是隔离的,多个 vhost 无法通讯,并且不用担心命名冲突(队列和交换器和绑定),实现了多层分离;
4、创建用户的时候必须指定 vhost;
5、vhost 操作
创建:rabbitmqctl addvhost[vhostname]
删除:rabbitmqctl deletevhost[vhostname]
查看:rabbitmqctl list_vhosts
6、最主要的目的是考虑到不同的分布式系统下面,如果我们有类似的业务场景,相应的可能会有相同名称的 exchange 和 queue,有了虚拟主机的概念就可以轻松区分了。
5.2.2 用户(User)
最直接了当的认证方式,谁可以使用当前的消息中间件。
5.2.3 交换器(Exchange)
交换器负责接收来自生产者的消息,将消息路由到 0 到多个队列。交换器的关键属性,主要包括:
1)名称(Name)。
2)类型:消息路由的规则,即由 “交换器类型” 和绑定规则,共同决定。
3)持久化(Durability):消息代理重启后,交换机是否还存在。交换机可以有两个状态:持久(durable)、暂存(transient)。持久化的交换机会在消息中间件(broker)重启后依旧存在,而暂存的交换机则不会(它们需要在消息中间件再次上线后重新被声明)。然而并不是所有的应用场景都需要持久化的交换机。
4)自动删除(Auto-delete):当所有与之绑定的消息队列都完成了对此交换机的使用后,是否自动删掉它。
5.2.3.1交换器类型
5.2.3.1.1 direct
工作机制
1、队列以绑定键 B 绑定到 direct 交换器。
2、消息(路由键 R)发送到 direct 交换器,若 B == R,消息即进入队列。
3、AMQP 提供了 “默认交换器”:类型为 direct,名称为空字符串。任何的队列被创建时,即以队列名称作为绑定键,绑定到 “默认交换器”。
5.2.3.1.2 fanout
fanout 交换器将消息路由到所有绑定的队列。类似于 “广播”。
5.2.3.1.3 topic
topic 交换器与 direct 交换器类似,基于消息路由键与队列绑定键进行匹配。区别在于,topic 交换器支持 “通配符” 形式的 “绑定键”:
1)“键” 以 . 划分成多个词
2)* 匹配任意 1 个词
3)# 匹配 0 到多个词:demo.rmq.example_1 与 *.rmq.example_1 和 demo.# 匹配。
5.2.3.1.4 header
与 direct 交换器类似,区别在于,header不依赖于消息路由键与队列绑定键的匹配,而是依赖于消息和绑定的 “headers” 匹配。
具体的匹配规则,依赖绑定的 “headers” 支持 x-match 属性:
1)all:默认值,当且仅当,消息 “headers” 与绑定 “headers”,全部 K-V 匹配
2)any:消息 “headers” 中任意 K-V,都能够与绑定 “headers” 匹配
3)“headers” 中,若以 x- 作为前缀,则不参与匹配计算。
5.2.4 消息队列(Queue)
队列接收来自交换器分发的消息,供消费者读取。队列的关键属性,包括:
1)队列名称
2)持久化:消息中间件重启后,队列是否依旧存在。持久化队列(Durable queues)会被存储在磁盘上,当消息中间件(broker)重启之后,它依旧存在。没有被持久化的队列称作暂存队列(Transient queues)。这里需要注意队列的持久化和它存储的未被消费消息的持久化是 2 个概念,队列的持久化并不会使存储的消息持久化。假如消息中间件(broker)重启之后,持久化队列会被重新声明,但它里面存储的消息只有设置过持久化的消息才能被重新恢复。
3)自动删除:队列没有 “订阅” 的消费者时,是否自动删除。
4)专用队列(Exclusive):可以这样理解当创建这个队列的 Connection 关闭后队列即被删除,不存在其它的使用可能性。
消息队列在声明(declare)后才能被使用。如果一个队列尚不存在,声明一个队列会创建它。如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。如果声明中的属性(名称除外)与已存在队列的属性有差异,那么则会申明失败。
5.2.5 消息(message)
消息的关键属性包括:
1)路由键:交换器路由消息的 “依据”。
2)投递模式:消息是否 “持久化”(消息代理重启后,交换器是否仍然 “存在”)。
3)Content-Type / Content-Encoding:通常作为 “载荷” 数据结构的标识。(“载荷” 即为消息传递的数据,其数据结构由应用决定,AMQP 保持透明,仅将其作为字节数组)
4)消息头(headers):消息的附加属性,K-V 结构。
为了达成消息的 “持久化”,消息、交换器、队列,必须全部 “持久化”。
5.2.6 路由键(RoutingKey)
路由关键字,交换机 exchange 的路由规则利用这个关键字进行消息投递到消息队列。(路由键长度不能超过 255 个字节)
用于把生成者的数据分配到交换器上。
5.2.7 绑定(Binding)
用于把交换器的消息绑定到队列上。
队列允许 “绑定” 到交换器,针对部分 “交换器类型”,绑定需要提供 “绑定键”(亦称为 “路由键”,区分于消息的 “路由键” 属性)。
BindingKey:用于把交换器的消息绑定到队列上。
6 消息持久化
6.1 概念
怎么保证 Rabbit 在重启的时候不丢失呢?答案就是消息持久化。
当你把消息发送到 Rabbit 服务器的时候,你需要选择你是否要进行持久化,但这并不能保证 Rabbit 能从崩溃中恢复,想要 Rabbit 消息能恢复必须满足以下条件:
1)投递消息的时候 durable 设置为 true,消息持久化,代码:
//参数2设置为true持久化;
channel.queueDeclare(x, true, false, false, null),
2)设置投递模式 deliveryMode 设置为 2(持久),代码:
//参数 3 设置为存储纯文本到磁盘;
channel.basicPublish(x, x, MessageProperties.PERSISTENTTEXTPLAIN,x),
3)消息已经到达持久化交换器上;
4)消息已经到达持久化的队列;
6.2 原理
Rabbit 会将你的持久化消息写入磁盘上的持久化日志文件,等消息被消费之后,Rabbit 会把这条消息标识为等待垃圾回收。
6.3 缺点
消息持久化的优点显而易见,但缺点也很明显,那就是性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量。
7 支持高可用 & 保障可靠性的方法
7.1 集群
RabbitMQ 支持集群,将多个物理节点构成单个逻辑层面的消息代理。
1)虚拟主机、交换器,自动地镜像到全部节点。
2)队列位于单个节点(连接到集群中任意的消息代理,全部队列都是可见的)。
7.2 队列镜像,高可用
1、“队列镜像”,将队列被 “镜像” 到不同的节点。
2、所有的队列,被区分为 “主”(master)和 “镜像”(mirror)。所有队列的操作,首先作用于 “主” 队列,进而扩散到 “镜像” 队列。当出现 “主” 队列故障,“镜像” 队列,基于特定机制,将升级成为 “主” 队列。
3、RabbitMQ 支持配置 “镜像” 队列的数量:指定数量、镜像到集群的全部节点、镜像到配置的节点。通常的建议,“主” 队列与 “镜像” 队列,数量构成集群节点数的 “quorum”(例如:2/3、3/5……)。
若 “主” 队列所在节点(“主” 节点)故障:
1、运行时间最长的 “同步” “镜像” 队列将升级成为 “主” 队列。
2、若没有 “同步” “镜像” 队列,则基于配置项 ha-promote-on-failure:
1)always,默认值,非 “同步” “镜像” 队列,允许升级成为 “主” 队列
2)when-synced,队列将不可用
3、当 “镜像” 队列新加入时,即处于非 “同步” 状态,其同步的方式,由于配置项 ha-sync-mode 控制:
1)manual,依赖于手动执行
2)automatic,自动同步
3)队列进行同步时,全部的队列操作将被阻塞,直到同步完成。
7.3 可靠性保障
消息不会 “丢失”,无论出现任何情况(异常、故障……)。
7.3.1 可靠性保障 - “生产者确认”
生产者的 “可靠性” 保障,主要依赖于 “生产者确认”(publisher confirm)模式:生产者完成消息发送,能够获得来自消息代理的确认。
@Component
public class ConfirmCallbackDemo implements RabbitTemplate.ConfirmCallback{
/**
* 通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中
* 1.如果消息没有到exchange,则 ack=false
* 2.如果消息到达exchange,则 ack=true
* @param correlationData
* @param ack
* @param cause
*/
public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause) {
System.out.println("[confirm]: id=" + correlationData.getId());
if(ack){// 成功接收
//todo 成功处理逻辑
}else{
// 失败原因
System.out.println("[confirm]: cause=" + cause);
//todo 失败处理逻辑
}
}
}
7.3.2 可靠性保障 - 确保消息被路由
若交换器无法将消息路由到任何队列,默认情况,消息将被 “丢弃”,特定的场景中,生产者需要感知。
@Component
public class ReturnCallbackDemo implements RabbitTemplate.ReturnCallback{
/**
* 当消息从交换机到队列失败时,该方法被调用。(若成功,则不调用)
* 需要注意的是:该方法调用后,MsgSendConfirmCallBack中的confirm方法也会被调用,且ack = true
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息主体 message : "+message);
System.out.println("消息主体 message : "+replyCode);
System.out.println("描述:"+replyText);
System.out.println("消息使用的交换器 exchange : "+exchange);
System.out.println("消息使用的路由键 routing : "+routingKey);
}
}
7.3.3 可靠性保障 - “消费者确认”
消费者示例代码,已阐述 “消费者确认”( consumer acknowledgement),注意,为保障可靠性:
1)不使用 “自动” 确认。
2)当且仅当消费者完成消息的处理,进行 “消费者确认”。
@Component
public class MessageHandler {
@RabbitListener(queues = "directqueue")
public void handleMessage(Message message, Channel channel) throws Exception{
try {
System.out.println("消费消息");
System.out.println(new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
} catch (Exception e){
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
}
}
}
8 死信队列
死信队列,又可以称之为“延迟/延时队列”,也是队列的一种,只不过与普通的队列最大的不同之处在于创建时的组成成分不同,创建死信队列的“成分”将不仅仅只是:名称、持久化、自动删除等基本属性,还包含了死信交换机、死信路由甚至还有TTL(Time-To-Live)即队列中消息可生存的时间。
8.1 作用
死信队列其实最大的作用是可以实现消息或者数据延迟/延时处理,而且还可以动态的设定延迟的时间,即动态设定 TTL。典型的业务场景很多,凡是业务中需要延迟一定时间再处理的数据均可以将其压入死信队列中,等待一定的时间后再执行真正的处理逻辑!
8.2 结构流程
在这里其实已经很明确的指出死信队列的创建跟绑定逻辑 以及 真正监听消费处理消息的队列的绑定逻辑。图中问题的答案为:当入死信队列的消息TTL一到,它自然而然的将被路由到死信交换机绑定的队列中被真正消费处理!!!
8.3 死信队列场景实战
用户在商城下单成功并点击去支付后在指定时间未支付时自动失效!
RabbitmqConfig 实施
死信队列:用于设定指定的待支付的交易订单号在指定的 TTL(单位为 ms)后何去何从!
真正队列:用于监听消费处理指定的交易订单号,即判断该交易订单号是否已完成,如果否,则失效之!
生产端的逻辑:用户商城下单的处理!
等待固定的 TTL:在这里设定的是 10s,当消息入死信队列 10s 后,将自然而然的将消息路由到下一个中转站,即真正的消费监听处理队列进行处理:判断该笔交易订单号是否已经付款,如果否,则失效之!
可以将该服务跑起来,然后发起 controller 的用户下单请求,会发现消息入死信队列后不会立马被消费,等待 10s 会,消息会被路由到真正的消费队列中进行处理,这一现象可以在 MQ 的后端控制台应用中看到!