RabbitMQ - 5 ( 17000 字 RabbitMQ 入门级教程 )

一:RabbitMQ 应用问题

1.1 幂等性保障

幂等性是数学和计算机科学中某些运算的一种性质,指的是这些运算可以被多次应用,而不会改变初始应用的结果。在应用程序中,幂等性是指对系统进行重复调用(相同参数),无论请求多少次,这些请求对系统的影响都是相同的。例如,数据库的 SELECT 操作是幂等的。虽然不同时间查询的结果可能不同,但查询操作对数据本身没有影响,幂等性关注的是对资源的影响,而不是返回的结果。

相反,某些操作是非幂等的,比如 i++ 操作。每次调用都会使变量的值发生变化。如果调用方没有控制好逻辑,同一流程被多次重复调用,结果就会不同。这种操作显然不符合幂等性要求,因此在设计系统时需要特别注意,确保关键操作具备幂等性以避免意外影响。

在 MQ 中,幂等性指的是同一条消息即使被多次消费,对系统的影响始终保持一致,不会因重复消费而对资源或状态造成多余的修改。一般来说,一般来说消息中间件的消息传输保障分为三个层级,分别对应不同的传输可靠性需求。

消息传输层级描述
At most once最多一次。消息可能会丢失,但绝不会重复传输。
At least once最少一次。消息绝不会丢失,但可能会重复传输。
Exactly once恰好一次。每条消息肯定会被传输一次且仅传输一次,保证消息的唯一性和可靠性。

RabbitMQ 支持 “最多一次” 和 “最少一次” 的消息传输保障,但对于 “恰好一次” 目前 RabbitMQ 还无法实现。这不仅是 RabbitMQ 的局限性,目前市面上的主流消息中间件也都难以完全实现 “恰好一次” 的保障。在实际业务中,对于可靠性要求较高的场景,建议选择 “最少一次”,以防止消息丢失。而“最多一次”可能因为网络问题或消费过程中的异常等原因,导致消息丢失,不适合对数据完整性要求较高的场景。

“最少一次”虽然可以防止消息丢失,但会带来一个问题:消费端可能会收到重复的消息,从而对同一条消息进行多次处理。这在一些不重要的业务场景中问题不大,但对于关键业务,如果不对重复消息进行有效处理,可能会导致严重后果。例如,当用户完成订单付款后,由于网络问题,付款成功的结果未能返回订单系统,用户再次点击付款时,如果系统未实现幂等性处理,就可能导致订单被多次扣款的情况,MQ消费者的幂等性的解决方法⼀般有以下两种:

实现方式描述
全局唯一ID1. 为每条消息分配唯一标识符(如 UUID 或 MQ 消息中的唯一 ID),确保唯一性。
2. 消费者收到消息后,根据 ID 判断消息是否已被消费。若已消费,则放弃处理。
3. 若未消费,开始处理消息,并在业务完成后将唯一 ID 保存到数据库或 Redis 中。
业务逻辑判断1. 检查数据库中是否存在相关数据记录,防止重复处理。
2. 使用乐观锁机制避免更新已被其他事务修改的数据。
3. 在处理消息前检查相关业务状态,确保操作尚未执行后再进行处理。

1.2 顺序性保障

消息的顺序性是指消费者消费消息的顺序与生产者发送消息的顺序保持一致。例如,生产者发送的消息顺序是 msg1、msg2、msg3,那么消费者也需要按照 msg1、msg2、msg3 的顺序进行消费。在许多业务场景中,消息消费无需保证顺序,比如通过 MQ 实现订单超时处理。但在某些业务场景中,消息的顺序处理至关重要,例如用户信息修改场景中,对同一个用户的同一资料进行多次修改时,必须保证消息按顺序处理,以确保数据的一致性和准确性。

在这里插入图片描述

一些资料提到 RabbitMQ 能够保障消息的顺序性,但这种说法并不严谨。在没有消息丢失、网络故障等异常的情况下,如果只有一个生产者和一个消费者,是可以保证消息顺序性的。然而,当有多个生产者同时发送消息时,由于无法确定消息到达 RabbitMQ Broker 的先后顺序,消息的顺序性就无法保证。实际使用中,有多种常见场景可能会打破 RabbitMQ 的消息顺序性。

场景描述
多个消费者当队列配置了多个消费者时,消息可能被不同消费者并行处理,从而导致消息处理顺序无法保证。
网络波动或异常在消息传递过程中,如果出现网络波动或异常,可能导致消息确认(ACK)丢失,使消息重新入队和消费,造成顺序性问题。
消息重试如果消费者未及时确认消息或确认丢失,MQ 可能认为消息未被成功消费并进行重试,从而导致消息顺序紊乱。
消息路由问题在复杂的路由场景中,消息可能根据路由键被发送到不同队列,无法保证全局的顺序性。
死信队列消息因消费端拒绝等原因被放入死信队列,死信队列被消费时无法保证消息顺序与生产者发送的顺序一致。

消息顺序性保障可以分为局部顺序性和全局顺序性两种。局部顺序性通常指在单个队列内保证消息的顺序,而全局顺序性则是指在多个队列或多个消费者之间保持消息顺序。在实际应用中,全局顺序性较难实现,通常需要通过业务逻辑来保证,例如在消息中嵌入序列号,并在消费端进行排序处理。相比之下,局部顺序性更常见,也更容易实现。

RabbitMQ 作为一个分布式消息队列,主要优化的是吞吐量和可用性,而不是严格的顺序性保障。如果业务场景确实需要严格的消息顺序性,通常需要在应用层面进行额外的设计和实现。接下来介绍一些常见的消息顺序性保障策略。

策略描述
单队列单消费者最简单的方法是使用单个队列,由单个消费者处理。同一个队列中的消息遵循先进先出(FIFO)原则,RabbitMQ 能够天然保证消息的顺序性。
分区消费当单个消费者的吞吐量不足时,可以将一个队列分割成多个分区,每个分区由一个消费者处理,从而提高处理速度,同时保持每个分区内消息的顺序性。
消息确认机制使用手动消息确认机制,消费者在处理完一条消息后显式发送确认,RabbitMQ 才会移除该消息并发送下一条消息,以此保证消息按照顺序被可靠地消费。
业务逻辑控制在某些场景中,即使消息乱序到达,也可以通过业务逻辑控制顺序性。例如,在消息中嵌入序列号,消费者根据序列号进行排序处理,以实现顺序消费。

RabbitMQ 本身并不保证全局的严格顺序性,特别是在分布式系统中。在实际应用开发中,需要根据具体的业务需求,灵活运用多种策略来实现所需的消息顺序性保障。

1.3 消息积压问题

消息积压是指在消息队列中,待处理的消息数量超过了消费者的处理能力,导致消息在队列中不断堆积的现象。消息积压可能会引发系统性能下降、用户体验受损,甚至系统崩溃。因此及时发现并解决消息积压问题对于维护系统的稳定性至关重要,消息积压通常由以下原因导致:

原因分类描述
消息生产过快在高流量或高负载情况下,生产者以极高的速率发送消息,超出了消费者的处理能力,导致消息堆积。
消费者处理能力不足消费者处理消息的速度跟不上生产者的消息发送速度,导致积压。可能原因包括:
1.消费端业务逻辑复杂,耗时较长
2.消费端代码性能较低
3.系统资源限制(如 CPU、内存、磁盘 I/O 等)
4.异常处理不当,消息无法被正确处理和确认。
网络问题由于网络延迟或不稳定,消费者无法及时接收或确认消息,最终导致消息积压。
RabbitMQ 配置偏低RabbitMQ 服务器资源或配置不足,限制了消息处理效率,从而导致消息积压。

解决方案如下:

解决方法描述
提高消费者效率1. 增加消费者实例数量,例如通过新增机器提高处理能力
2. 优化业务逻辑,例如使用多线程处理任务,提高单个消费者的效率
3. 设置 prefetchCount,当一个消费者阻塞时,将消息转发到其他未阻塞的消费者
4. 当消息发生异常时,设置合适的重试策略,或将其转入死信队列,避免阻塞主队列。
限制生产者速率1. 流量控制:在生产者中实现流量控制逻辑,根据消费者的处理能力动态调整消息发送速率
2. 限流:使用限流工具为生产速率设置上限,避免超出消费者处理能力
3. 设置过期时间:为消息设置过期时间,未消费的消息可配置进入死信队列,减轻主队列压力并防止消息丢失。
资源与配置优化优化 RabbitMQ 服务器配置或升级硬件,例如增加内存、CPU、磁盘 I/O 性能,并调整 RabbitMQ 的配置参数以提高消息处理效率。

二: RabbitMQ 运维

前面介绍的 RabbitMQ 安装与运行都是单机版,这种模式无法满足实际应用的高可靠性和高性能需求。想象一下,如果 RabbitMQ 服务器因内存崩溃、断电或主板故障等情况而停止运行,该如何应对?此外,单台 RabbitMQ 服务器的吞吐量可能只能满足每秒 1000 条消息,而如果应用需要每秒处理 10 万条消息,仅通过购买昂贵的硬件提升单机性能并不是理想的解决方案。在这种情况下,搭建 RabbitMQ 集群是更高效的选择。RabbitMQ 集群不仅能够在单个节点崩溃时保持消费者和生产者的正常运行,还可以通过增加更多节点来线性扩展消息通信的吞吐量。即使某个节点宕机,客户端也可以重新连接到其他节点继续工作。

然而,RabbitMQ 集群并不能完全保证消息的绝对可靠性。即便将消息、队列、交换器等配置为持久化,并且生产端和消费端正确使用了确认机制,当集群中的某个节点崩溃时,该节点上的队列消息仍有可能丢失。虽然集群中的所有节点都会备份元数据(如队列、交换机的名称及属性、绑定关系和 vhost 等),但默认情况下不会备份消息。这一问题可以通过一些配置来解决。接下来,我们将讨论如何正确且高效地搭建一个 RabbitMQ 集群。

2.1 多机多节点

RabbitMQ 集群对网络延迟非常敏感,因此在搭建 RabbitMQ 集群时,建议将多个节点部署在同一局域网内。接下来,我们将使用三台位于同一局域网内的云服务器来搭建 RabbitMQ 集群。

2.1.1 安装 RabbitMQ

在三台服务器上分别安装 RabbitMQ,并确保每个服务器节点都可以正常运行且可用,我们安装后各个节点的信息如下:

节点IP开放端口节点名称
节点110.0.0.2325672, 15672rabbit@iZ2vc7a1n9gvhfp589oav8Z
节点210.0.0.2335672, 15672rabbit@iZ2vc7a1n9gvhfp589oav6Z
节点310.0.0.2345672, 15672rabbit@iZ2vc7a1n9gvhfp589oav7Z

2.1.2 配置 hosts 文件

为确保各节点能够相互识别,需要配置每个节点的 hosts 文件,按照格式 IP 节点名称 添加节点信息,通过编辑 vim /etc/hosts 文件,添加以下配置:

#rabbitmq
10.0.0.232 iZ2vc7a1n9gvhfp589oav8Z
10.0.0.233 iZ2vc7a1n9gvhfp589oav6Z
10.0.0.234 iZ2vc7a1n9gvhfp589oav7Z

2.1.3 配置 Erlang Cookie

RabbitMQ 节点和 CLI 工具(如 rabbitmqctl)使用 Cookie 进行身份验证,以确认它们之间是否被允许相互通信。为了使多个节点能够正常通信,它们必须具有相同的共享密钥,即 Erlang Cookie。Cookie 是一个字符串,通常存储在本地文件中,每个集群节点都需要使用相同的 Cookie。RabbitMQ 启动时,Erlang 虚拟机会自动创建该文件,通常存放在 /var/lib/rabbitmq/.erlang.cookie 或 $HOME/.erlang.cookie 目录下。

2.1.3.1 停止所有节点的服务
systemctl stop rabbitmq-server
2.1.3.2 配置 Erlang Cookie

只需将一个节点上的 .erlang.cookie 文件复制到其他两个节点对应的位置即可,例如可以将 node3 节点上的 .erlang.cookie 文件分别复制到 node1 和 node2 的对应目录中。在 node3 上执行复制操作即可完成。

2.1.3.4 启动节点

以后台运行的方式启动三台 RabbitMQ 服务,使用以下启动命令:

rabbitmq-server -detached 
启动命令描述
rabbitmq-server启动 RabbitMQ 服务的命令。
-detached参数表示以后台模式运行,将 RabbitMQ 作为服务在后台启动。

2.1.4 构建集群

要将集群中的三个节点连接起来,需要让 node1 和 node2 加入到 node3 节点。在加入 node3 之前,必须先重置两个新加入的节点,即 node1 和 node2。可以分别在 node1 和 node2 的机器上执行相关的重置命令来完成操作。

#1. 关闭 RabbitMQ 服务
rabbitmqctl stop_app

#2. 重置当前节点,重置节点会删除该节点上以前存在的所有资源和数据
rabbitmqctl reset

#3.加⼊节点 后⾯跟的是 node3 节点
rabbitmqctl join_cluster rabbit@iZ2vc7a1n9gvhfp589oav7Z

#4. 启动服务
rabbitmqctl start_app

2.1.5 查看集群状态

可以通过命令或者管理平台看到集群信息

rabbitmqctl cluster_status

在这里插入图片描述

2.2 单机多节点

此处我们讲解在 Ubuntu 搭建 RabbitMQ 单机多节点

2.2.1 安装 RabbitMQ

可以参考前面的步骤,如果已经安装过了此步可以省略,下面提一下 RabbitMQ 中常见的端口号:

端口号描述
25672Erlang 分布式节点通信的默认端口,Erlang 是 RabbitMQ 的底层通信协议。
15672Web 管理界面的默认端口,通过该端口访问 RabbitMQ 的 Web 管理控制台,用于查看和管理消息队列。
5672AMQP 协议的默认端口,用于客户端与 RabbitMQ 服务器之间的通信。

安装完后启动两个节点,节点名称和端口号分别设置为:

节点名称AMQP 协议端口号Web 管理界面端口号
rabbit2567315673
rabbit3567415674

接着通过下面的命令启动:

RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME=rabbit2 rabbitmq-server -detached

RABBITMQ_NODE_PORT=5674 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]" RABBITMQ_NODENAME=rabbit3 rabbitmq-server -detached

接下来分别测试以下地址是否可以正常访问 RabbitMQ 的 Web 管理界面:

  • http://124.71.229.73:15672/
  • http://124.71.229.73:15673/
  • http://124.71.229.73:15674/

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2.2 搭建集群

2.2.2.1 停止服务并重置
rabbitmqctl -n rabbit2 stop_app
rabbitmqctl -n rabbit2 reset

rabbitmqctl -n rabbit3 stop_app
rabbitmqctl -n rabbit3 reset
2.2.2.2 把 rabbit2, rabbit3 添加到集群
root@hcss-ecs-2618:~# rabbitmqctl -n rabbit2 join_cluster rabbit@hcss-ecs-2618 Clustering node rabbit2@hcss-ecs-2618 with rabbit@hcss-ecs-2618
root@hcss-ecs-2618:~# rabbitmqctl -n rabbit3 join_cluster rabbit@hcss-ecs-2618 Clustering node rabbit3@hcss-ecs-2618 with rabbit@hcss-ecs-2618
2.2.2.3 重启 rabbit2,rabbit3 并查看状态
root@hcss-ecs-2618:~# rabbitmqctl -n rabbit2 start_app Starting node rabbit2@hcss-ecs-2618 ...
root@hcss-ecs-2618:~# rabbitmqctl -n rabbit3 start_app Starting node rabbit3@hcss-ecs-2618 ...

rabbitmqctl cluster_status -n rabbit

在这里插入图片描述

2.3 宕机演示

安装完成后可能会出现数据不同步的问题,我们可以看一下是什么原因:

  1. 分别以 rabbit 节点和 rabbit2 节点添加两个队列

在这里插入图片描述

步骤描述
选择虚拟机(需确保当前用户对虚拟机具有操作权限)。
设置队列名称。
是否启用持久化。
指定主节点,其他节点作为从节点。
  1. 添加之后可以看到三个节点都有队列了

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 往任意一个节点的 testQueue 队列中发送⼀条数据

在这里插入图片描述

  1. 发送之后, 观察 3 个节点的队列中均有消息

在这里插入图片描述

  1. 关闭主节点
rabbitmqctl -n rabbit stop_app   #rabbit为节点名称 Stopping rabbit application on node rabbit@hcss-ecs-2618 ...
  1. 关闭后可以看到 rabbit2 和 rabbit3 没有该队列的数据了,但是另⼀个队列不受影响

在这里插入图片描述

也就是说数据只存在于主节点,从节点并没有存储数据。如果关闭的是 rabbit2,那么 testQueue2 的数据将会丢失。为了解决这一问题,需要引入"仲裁队列"来保证数据的高可用性和可靠性。

2.4 仲裁队列

RabbitMQ 的仲裁队列是一种基于 Raft 一致性算法实现的持久化、复制的 FIFO 队列,提供队列数据复制能力,保障数据的高可用性和安全性。通过仲裁队列,RabbitMQ 可以在多个节点间复制队列数据,即使某个节点宕机,队列仍能继续提供服务,从而提升系统的可靠性和容错能力。

2.4.1 Raft 算法

Raft 是一种用于管理和维护分布式系统一致性的协议,它是一种共识算法,旨在实现高可用性和数据的持久性。通过在节点间复制数据,Raft 能够保证分布式系统中的一致性,即使在节点故障的情况下,也能确保数据不会丢失。为了消除单点故障并提高系统的可用性,分布式系统通常使用数据副本进行容错,但这也引发了如何保证多个副本之间一致性的问题。

共识算法(Consensus Algorithm)正是为了解决这一问题,允许多个分布式节点就某个值或一系列值达成一致性协议。即使在某些节点发生故障、网络分区或其他异常的情况下,共识算法仍能保证系统的一致性和数据的可靠性。常见的共识算法包括 Paxos、Raft 和 Zab 等。

算法描述
Paxos一种经典的共识算法,用于解决分布式系统中的一致性问题。
Raft一种较新的共识算法,相较于 Paxos,Raft 对其进行了简化和改进,旨在更易于理解和实现。
ZabZooKeeper 使用的共识算法,基于 Paxos 算法。与 Raft 的主要区别在于:
1. 对 Leader 任期的定义,Raft 称为 term,Zab 称为 epoch
2. 在状态复制过程中,Raft 的心跳由 Leader 发送到 Follower,而 Zab 的心跳则相反。
Gossip一种对等分布式算法,没有节点角色区分。每个节点将数据变动通知其他节点,传播方式类似“传八卦”。

2.4.2 Raft 基本概念

Raft 使用 Quorum 机制来实现共识和容错,即对 Raft 集群的操作必须获得大多数节点(大于 N / 2)的同意才能提交。节点是指分布式系统中的独立成员。当客户端向 Raft 集群发起一系列读写操作时,所有操作都需要通过主节点(Leader)处理,因此选举主节点是 Raft 核心算法中的第一步。如果没有主节点,集群将无法工作,因此必须先通过 Leader 选举完成主节点的选定,之后才能继续处理其他任务。

主节点的职责是接收客户端的操作请求,将这些操作包装成日志并同步给其他节点。在大多数节点完成日志同步后,主节点可以安全地向客户端返回响应。这部分工作在 Raft 核心算法中称为日志复制(Log replication)。由于主节点的责任非常重要,只有符合条件的节点才有资格当选。为确保集群对外一致性,主节点在处理日志时需要严格保证安全性(Safety)。Raft 算法将一致性问题拆分为三个子问题:Leader 选举、日志复制和安全性。接下来,我们将详细介绍 Raft 的 Leader 选举过程。

2.4.3 选主

Raft 动画演示在线地址,选主是指在集群中选出一个主节点,负责处理特定的任务和管理工作。在完成选主过程后,集群中的每个节点都会识别出唯一的节点作为 Leader。在 Raft 算法中,每个节点始终处于以下三种角色之一:

节点角色描述
Leader(领导者)负责处理所有客户端请求,并将这些请求作为日志条目复制到所有 Follower。Leader 定期向所有 Follower 发送心跳消息,以维持其领导地位并防止 Follower 进入选举过程。在正常情况下集群只有一个 Leader,剩下的都是 Follower。
Follower(跟随者)接收来自 Leader 的日志条目,并在本地应用这些条目。Follower 不直接处理客户端请求。
Candidate(候选者)当 Follower 在一定时间内未收到 Leader 的心跳消息时,会怀疑 Leader 是否仍然可用。在这种情况下,Follower 转变为 Candidate,并通过投票尝试成为新的 Leader。

在这里插入图片描述

可以看出所有节点在启动时默认处于 Follower 状态。如果在一定时间内未收到来自 Leader 的心跳信号,节点会从 Follower 切换到 Candidate 并发起选举。若 Candidate 获得多数派(包括自己的一票)的投票支持,则切换为 Leader 状态。Leader 通常会一直正常工作,直到出现异常或故障。

2.4.4 任期

Raft 将时间划分为任意长度的任期(term),每个任期从一次选举开始。在选举过程中,一个或多个 Candidate 尝试成为 Leader。成功完成选举后,选出的 Leader 将管理集群直至任期结束。在某些情况下,选举可能无法选出 Leader,此时该任期将以无 Leader 的状态结束(如 t3 所示)。随后,一个新的任期和选举会迅速重新开始。

在这里插入图片描述

Term 更像是一个逻辑时钟,用于判断节点的状态是否过期。每个节点都存储一个当前任期号(current term number),该任期号会随着时间单调递增。节点之间通信时会交换各自的当前任期号。如果某个节点发现自己的任期号小于其他节点,会将任期号更新为较大的值。如果 Candidate 或 Leader 发现自己的任期号过期,会立即切换回 Follower 状态;而当节点接收到一个带有过期任期号的请求时,则会直接拒绝该请求。

在 Raft 算法中,服务器节点之间通过 RPC 进行通信,主要涉及两类 RPC 请求,用于实现选举和日志复制的核心机制。

RPC 请求类型描述
RequestVote RPCs请求投票,由 Candidate 在选举过程中发出,用于争取成为 Leader。
AppendEntries RPCs追加条目,由 Leader 发出,用于日志复制以及提供心跳机制以维持领导地位。

2.4.5 选举过程

Raft 使用心跳机制来触发 Leader 选举。当服务器启动时,所有节点都处于 Follower 状态。如果 Follower 在选举超时时间内没有收到来自 Leader 的心跳信号,此时可能是尚未选出 Leader、Leader 故障,或 Leader 与 Follower 之间的网络异常,接着 Follower 会主动发起选举。

在这里插入图片描述

步骤描述
1率先超时的节点自增当前任期号,切换为 Candidate 状态,并为自己投一票。
2以并行方式向集群中的其他服务器节点发送 RequestVote RPCs,请求得到它们的投票。
3等待其他节点的回复,根据投票结果决定是否成为 Leader 或继续保持 Candidate 状态。

投票的规则是每个服务器节点按照先来先服务的原则(first-come-first-served)只投给一个 Candidate。同时,Candidate 必须保证其掌握的信息不少于投票节点的信息,才能获得投票支持。

在这里插入图片描述

在这个过程中可能出现三种结果:

情况描述
1赢得选举(包括自己的投票),成为 Leader。
2其他节点赢得选举,当前节点自动切换为 Follower。
3在一定时间内未收到多数派(majority)投票,保持 Candidate 状态,并重新发起选举。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
第三种情况如果没有额外的措施, 这种无结果的投票可能会无限重复下去,为了解决选举中可能出现的无结果投票问题,Raft 通过随机选举超时时间机制来减少此类情况的发生,并确保即使发生也能迅速解决。选举超时时间从一个固定的时间区间(例如 150-300ms)中随机选择,以避免选票一开始被瓜分。通过这种方式,服务器的超时分布得以分散,大多数情况下只有一个服务器会率先超时,从而赢得选举,并在其他服务器超时之前发送心跳信号。

2.4.6 Raft 协议下的消息复制

每个仲裁队列由多个副本组成,包括一个主副本和多个从副本。例如,副本因子为 5 的仲裁队列包含 1 个主副本和 4 个从副本,每个副本分布在不同的 RabbitMQ 节点上。客户端(生产者和消费者)只与主副本交互,主副本将操作命令复制到从副本。当主副本所在节点下线时,从副本中会选举出一个新的主副本继续提供服务。消息复制和主副本选举需要超过半数的副本同意。当生产者发送消息时,只有超过半数的队列副本将消息写入磁盘后,才会向生产者确认,这种机制可以避免少部分较慢的副本对整个队列性能的影响。

在这里插入图片描述

2.5 仲裁队列

2.5.1 创建仲裁队列

  1. 使用 Spring 框架代码创建
@Bean("quorumQueue")
public Queue quorumQueue() {
    return QueueBuilder
            .durable("quorum_queue") // 设置队列名称为 quorum_queue,持久化
            .quorum()                // 配置为仲裁队列
            .build();                // 构建队列
}
  1. 使用 amqp-client 创建
Map<String, Object> param = new HashMap<>();
param.put("x-queue-type", "quorum");
channel.queueDeclare("quorum_queue",true,false,false,param);
  1. 使用管理平台创建,创建时选择 Type 为 Quorum 指定主副本

在这里插入图片描述

2.5.2 创建后观察管理平台

在这里插入图片描述

在仲裁队列的名称后可以看到一个“+2”的标记,表示该队列有 2 个镜像节点。仲裁队列默认的镜像数量为 5,即 1 个主节点和 4 个从副本节点。如果集群中的节点数量少于 5,例如搭建了 3 个节点的集群,那么创建的仲裁队列将为 1 主 2 从;如果集群中的节点数量大于 5,那么仲裁队列只会在 5 个节点中创建 1 主 4 从。点击队列名称后,可以查看其详细信息。

在这里插入图片描述

可以看到当集群中有多个仲裁队列时,主副本和从副本会分布在集群的不同节点上,每个节点可以同时承载多个主副本和从副本。仲裁队列的消息发送和接收操作与普通队列完全相同。

仲裁队列是 RabbitMQ 从 3.8.0 版本引入的一种新型队列类型,与普通队列相比,仲裁队列在分布式环境下对消息的可靠性保障更高。普通队列只存放在集群中的一个节点上,虽然其他节点可以通过转发请求访问该队列,但一旦队列所在节点宕机,队列中的消息就会丢失,因此普通集群仅提升了并发能力,并未实现高可用性。而仲裁队列通过在多个节点间复制数据,极大地保障了 RabbitMQ 集群的高可用性和数据可靠性。

2.6 HAProxy 负载均衡

面对大量业务访问和高并发请求,可以通过高性能服务器提升 RabbitMQ 的负载能力。当单机容量达到极限时,可以通过集群策略进一步提升负载能力。然而,这种方式也会带来一些问题。例如,在一个包含 3 个节点的集群中,应用程序应如何选择访问的节点?实际上,可以访问集群中的任何一个节点,但这也会引发两个潜在问题。

问题描述
节点故障导致程序问题如果访问的是 node1,但 node1 挂了,程序也会出现问题,因此需要一个统一的入口,当某个节点故障时流量可以及时转移到其他节点。
负载分布不均如果所有客户端都连接到 node1,则 node1 的网络负载会大大增加,而其他节点因负载较少导致硬件资源被浪费。

在这种情况下,引入负载均衡显得尤为重要。通过负载均衡,客户端的连接可以均匀分配到集群中的各个节点,避免单点故障和负载集中造成的资源浪费问题,从而提高系统的稳定性和资源利用率。针对 RabbitMQ 集群的负载均衡,可以采用客户端内部实现负载均衡或使用 HAProxy、LVS 等软件负载均衡技术。这里将重点介绍如何使用 HAProxy 来实现负载均衡。

在这里插入图片描述

2.6.1 在 Ubuntu 上安装 HAProxy

  1. 安装 HAProxy
#更新软件包
sudo apt-get update

#查找haproxy
sudo apt list|grep haproxy

#安装haproxy
sudo apt-get install haproxy
  1. 验证安装
#查看服务状态
sudo systemctl status haproxy

#查看版本
haproxy -v

#如果要设置HAProxy服务开机⾃启,可以使⽤:
sudo systemctl enable haproxy
  1. 通过 vim /etc/haproxy/haproxy.cfg 在 haproxy.cfg 追加以下内容:
# HAProxy Web 管理界面配置
listen stats
    bind *:8100                       # 绑定管理界面的监听端口为 8100
    mode http                         # 配置管理界面以 HTTP 模式运行
    stats enable                      # 启用 HAProxy 的统计功能
    stats realm Haproxy\ Statistics   # 设置管理界面的标题为 "Haproxy Statistics"
    stats uri /                       # 设置访问管理界面的 URI 为 "/"
    stats auth admin:admin            # 设置访问管理界面的认证信息(用户名:密码)

# RabbitMQ 负载均衡配置
listen rabbitmq
    bind *:5670                       # 绑定 RabbitMQ 客户端连接的负载均衡端口为 5670
    mode tcp                          # 使用 TCP 模式处理 RabbitMQ 的连接
    balance roundrobin                # 使用轮询算法分配客户端请求到后端节点
    server rabbitmq1 127.0.0.1:5672 check inter 5000 rise 2 fall 3
                                       # 定义后端节点 rabbitmq1,连接地址为 127.0.0.1:5672
                                       # 使用健康检查,检查间隔为 5000ms
                                       # rise 表示连续 2 次健康检查成功后标记为可用
                                       # fall 表示连续 3 次健康检查失败后标记为不可用
    server rabbitmq2 127.0.0.1:5673 check inter 5000 rise 2 fall 3                                    
    server rabbitmq3 127.0.0.1:5674 check inter 5000 rise 2 fall 3
  1. 重启 HAProxy 后访问 http://124.71.229.73:8100/
sudo systemctl restart haproxy

在这里插入图片描述

2.6.2 使用

  1. 修改配置文件:
spring:
  rabbitmq:
    addresses: amqp://study:study@124.71.229.73:5670/bite
    # 配置 RabbitMQ 的连接地址:
    # amqp:// 表示使用 AMQP 协议连接
    # study:study 为用户名和密码
    # 124.71.229.73 为 HAProxy 的 IP 地址
    # 5670 为 HAProxy 配置的负载均衡端口
    # /bite 表示虚拟主机(vhost),用于隔离不同的应用
  1. 声明队列 test_cluster
public static final String CLUSTER_QUEUE = "cluster_queue";
@Configuration
public class ClusterConfig {

    // 定义集群队列
    @Bean("clusterQueue")
    public Queue clusterQueue() {
        return QueueBuilder
                .durable(Constant.CLUSTER_QUEUE) // 设置队列名称为 CLUSTER_QUEUE,持久化
                .quorum()                        // 配置为仲裁队列
                .build();                        // 构建队列
    }
}
  1. 发送消息
@RestController
@RequestMapping("/cluster")
public class ClusterController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 发送消息到集群队列
    @GetMapping("/send")
    public String sendClusterMessage() {
        rabbitTemplate.convertAndSend("", Constant.CLUSTER_QUEUE, "quorum test...");
        return "发送成功!";
    }
}
  1. 测试

在这里插入图片描述
在这里插入图片描述

  1. 我们停止其中⼀个节点, 继续测试步骤 2 的代码

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在节点 rabbit 宕机的情况下,继续发送消息可以看到消息依然发送成功。通过查看界面,可以发现队列中显示有两条数据。

  1. 集群恢复
rabbitmqctl -n rabbit start_app

在这里插入图片描述

2.7 RabbitMQ 总结

2.7.1 MQ 的应用场景

消息队列(MQ)是一种应用程序之间的通信方式,允许系统组件以异步方式进行交互。在不同的应用场景下,消息队列能够发挥不同的作用,常见的应用场景包括以下几种:

场景描述
异步解耦在业务流程中,将一些耗时但无需立即返回结果的操作异步化。例如,用户注册后发送短信或邮件通知可以作为异步任务处理,而无需等待操作完成再告知用户注册成功。
流量削峰应对访问量剧增的场景,如秒杀或促销活动。使用 MQ 控制流量,将请求排队,根据系统处理能力逐步处理请求,从而避免因突发流量导致系统崩溃。
异步通信在无需即时处理消息的场景下,MQ 提供异步机制,允许应用将消息存入 MQ 中并延后处理,提高系统灵活性和响应速度。
消息分发当多个系统需要对同一数据做出响应时,使用 MQ 实现消息分发。例如支付成功后,支付系统向 MQ 发送消息,其他系统订阅消息并执行相关操作,无需轮询数据库。
延迟通知在需要延迟一段时间发送通知的场景下使用 MQ,例如在电商平台中,用户下单后未及时支付,可以通过延迟队列在超时后自动取消订单并通知用户。

2.7.2 不同 MQ 的区别

目前业界有多种 MQ 产品,例如 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ 等,每个消息队列各有侧重,实际选型时需结合自身需求和 MQ 产品特性综合考虑,选择最适合的解决方案。以下简单介绍其中三种:

消息队列简介
Kafka起初用于日志收集和传输,追求高吞吐量,性能卓越,单机吞吐可达十万级,功能相对简单,主要支持基本的 MQ 功能。适合大数据处理、日志聚合、实时分析等场景,在日志领域较为成熟。
RabbitMQ使用 Erlang 开发,MQ 功能较完备,支持几乎所有主流语言,提供友好的开源管理界面,性能较好,吞吐量可达万级。社区活跃,文档更新频繁,适合中小型公司,尤其是数据量和并发量不太高的场景。
RocketMQ使用 Java 开发,由阿里巴巴开源并捐赠给 Apache。可用性、可靠性、稳定性表现优异,吞吐量可达十万级,在大规模分布式系统中表现出色。广泛应用于互联网金融等高并发、高可靠性场景,但客户端语言支持较少,文档和社区活跃度相对一般。

2.7.3 RabbitMQ 的核心概念及工作流程

RabbitMQ 是一款消息中间件,也是典型的生产者-消费者模型。它的主要功能是接收、存储和转发消息。根据其工作流程图介绍一下其核心概念。

核心概念描述
Producer生产者,负责向 RabbitMQ 发送消息。
Consumer消费者,负责从 RabbitMQ 接收并处理消息。
Broker消息队列服务器或服务实例,即 RabbitMQ Server。
Connection网络连接,用于客户端与 RabbitMQ 之间的通信。
Channel网络连接中的虚拟通道,所有消息的发送和接收都通过通道进行。
Exchange交换机,接收生产者发送的消息,并根据路由规则将消息分发到一个或多个队列。
Queue消息队列,负责存储消息,直到消息被消费者消费。

在这里插入图片描述

工作流程如下:

步骤描述
创建连接Producer 连接到 RabbitMQ Broker,建立一个连接(Connection),并开启一个信道(Channel)。
声明交换机和队列Producer 声明一个交换机(Exchange)和队列(Queue),并绑定队列到交换机,定义消息的路由规则。
发布消息Producer 将消息发送到 RabbitMQ Broker。
消息存储RabbitMQ Broker 接收消息,并将其存入相应的队列中。如果未找到对应的队列,根据生产者配置选择丢弃或退回消息。
消费消息Consumer 监听队列(Queue),当消息到达时,从队列中获取消息并处理,随后向 RabbitMQ 发送确认信息。
消息删除消息被确认后,RabbitMQ 将消息从队列中删除。

2.7.4 RabbitMQ 如何保证消息的可靠性

场景可能原因解决办法
生产者将消息发送到 RabbitMQ 失败网络问题等发送方确认-confirm确认模式
消息在交换机中无法路由到指定队列代码或配置层面错误,导致消息路由失败发送方确认-return模式
消息队列自身数据丢失消息到达 RabbitMQ 后,RabbitMQ 服务器宕机导致消息丢失开启 RabbitMQ 持久化机制,消息会写入磁盘;在服务器恢复后,RabbitMQ 会自动读取之前存储的数据。
在极端情况下(消息未持久化时服务器宕机),可能会导致少量数据丢失,可以通过集群方式提升可靠性。
消费者异常导致消息丢失消息到达消费者后未及时消费,消费者宕机或消费者逻辑问题消息确认。启用消费者手动确认机制,当消费者确认消息成功后才会删除消息,从而避免消息丢失。
除此之外可配置重试机制,确保消息消费的可靠性。

2.7.5 RabbitMQ 是推模式还是拉模式

RabbitMQ 支持两种消息传递模式:推模式(push)和拉模式(pull)。

模式描述优点应用场景
推模式消息中间件主动将消息推送给消费者。消息获取更加实时,适合对数据实时性要求较高的场景。实时数据处理,如监控系统、报表系统等。
拉模式消费者主动从消息中间件拉取消息。消费端可以根据自身处理能力消费消息,避免消息积压和资源浪费,适合需要流量控制的场景。流量控制、大量计算资源任务,如批量处理任务等。

RabbitMQ 主要基于推模式工作,其核心设计是让消费者接收到由生产者发送的消息。通过调用 channel.basicConsume 方法订阅队列,RabbitMQ 会自动将消息推送给订阅该队列的消费者。如果只需要从队列中获取单条消息而非持续订阅,可以使用 channel.basicGet 方法进行消息消费。接下来通过代码演示推模式和拉模式的实现。

  1. 生产消息
// 1. 创建 channel 通道
Channel channel = connection.createChannel();

// 2. 声明队列
channel.queueDeclare(
    "message_queue", // 队列名称
    true,            // 是否持久化
    false,           // 是否排他
    false,           // 是否自动删除
    null             // 其他参数
);

// 3. 通过 channel 发送消息到队列中
String msg = "hello message~~";
for (int i = 0; i < 10; i++) {
    channel.basicPublish(
        "",                // 交换机名称(空字符串表示默认交换机)
        "message_queue",   // 队列名称
        null,              // 消息属性
        msg.getBytes()     // 消息内容
    );
}

在这里插入图片描述

  1. 拉模式
// 1. 创建 channel 通道
Channel channel = connection.createChannel();

// 2. 声明队列并绑定,指定消费的队列
channel.queueDeclare(
    "message_queue", // 队列名称
    true,            // 是否持久化
    false,           // 是否排他
    false,           // 是否自动删除
    null             // 其他参数
);

// 3. 使用 Pull 模式获取消息
GetResponse getResponse = channel.basicGet("message_queue", true); // 第二个参数 true 表示自动确认
if (getResponse != null) {
    System.out.println(new String(getResponse.getBody())); // 打印消息内容
} else {
    System.out.println("队列中无消息");
}

// 4. 释放资源
channel.close();
connection.close();

运行后观察控制台会发现成功打印一条信息,此时队列还剩 9 条消息

在这里插入图片描述

  1. 推模式
// 1. 创建 channel 通道
Channel channel = connection.createChannel();

// 2. 声明队列
channel.queueDeclare(
    "message_queue", // 队列名称
    true,            // 是否持久化
    false,           // 是否排他
    false,           // 是否自动删除
    null             // 其他参数
);

// 3. 接收消息并消费
DefaultConsumer consumer = new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, 
                               AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println("接收到消息: " + new String(body));
    }
};

// 4. 启动消费
channel.basicConsume(
    "message_queue", // 队列名称
    true,            // 自动确认
    consumer         // 消费者
);

// 5. 等待消费结束后释放资源
Thread.sleep(2000); // 模拟等待消息消费完成
channel.close();
connection.close();

可以发现队列中的消息已经为空了

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ice___Cpu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值