单机多节点
在我们的服务器上先安装 RabbitMQ
查看 RabbitMQ 状态


25672 是 Erlang 分布式节点通信的默认端口,Erlang 是 RabbitMQ 的底层通信协议
15672 是 web 管理界面的默认端口,通过这个端口可以访问 RabbitMQ 的 Web 管理控制台,用于查看和管理消息队列
5672 是 AMQP 协议的默认端口,用于客户端 与 RabbitMQ 服务器之间的通信
在启动两个节点
5673 和 5674
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
验证启动成功


要提前在服务器开放这俩端口号
搭建集群
停止服务并重置
rabbitmqctl -n rabbit2 stop_app
rabbitmqctl -n rabbit2 reset
rabbitmqctl -n rabbit3 stop_app
rabbitmqctl -n rabbit3 reset

把 rabbit2,rabbit3 添加到集群

@iZuf62rzke9h83x39cgn15Z 这个就是 node name
rabbitmqctl -n rabbit2 join_cluster rabbit@iZuf62rzke9h83x39cgn15Z
Clustering node rabbit2@iZuf62rzke9h83x39cgn15Z with rabbit@iZuf62rzke9h83x39cgn15Z

rabbitmqctl -n rabbit3 join_cluster rabbit@iZuf62rzke9h83x39cgn15Z
Clustering node rabbit3@iZuf62rzke9h83x39cgn15Z with rabbit@iZuf62rzke9h83x39cgn15Z
重启 rabbit2,rabbit3
rabbitmqctl -n rabbit2 start_app
Starting node rabbit2@iZuf62rzke9h83x39cgn15Z ...
rabbitmqctl -n rabbit3 start_app
Starting node rabbit3@iZuf62rzke9h83x39cgn15Z ...


这样看不直观,也可以在主节点管理界面看到集群其他节点

宕机演示
添加了节点之后,各个节点的数据是不同步的
1.添加队列




我们试着给这个队列发送一个消息

发送之后三个节点的队列都有消息

关闭主节点

rabbitmqctl -n rabbit stop_app
Stopping rabbit application on node rabbit@iZuf62rzke9h83x39cgn15Z ...

关闭后可以看到 rabbit2 和 rabbit3 没有该队列的数据了


也就是说这个数据只有 rabbit 有,因为这个队列就是 rabbit 创建的
要解决这个问题就要引入 仲裁队列了
仲裁队列
**RabbitMQ 的仲裁队列是一种基于 Raft 一致性算法实现的队列,仲裁队列提供队列复制的能力,**保障数据的高可用性和安全性,使用仲裁队列可以在 RabbitMQ 节点间进行队列数据的复制,从而达到在一个节点宕机时,队列仍然可以提供服务的效果
仲裁队列是 RabbitMQ 3.8 版本最为重要的改动,他是镜像队列的替代方案
**Raft 是一种用于管理和维护分布式系统一致性的协议,他是一种共识算法,**旨在实现高可用性和数据持久性,Raft 通过在节点间复制数据来保证分布式系统的一直性,即使在节点故障的情况下也能保证数据不会丢失
我们对 Raft 集群的操作必须得到大多数节点的同意才能提交
Raft 集群必须存在一个主节点,客户端向集群发起的所有操作都必须经由主节点处理,所以 Raft 核心算法中的一部分就是选主
主节点会负责接收客户端发过来的操作请求,将操作请求包装为日志同步给其他节点,在保证大部分节点都同步了本次操作后,就可以安全的给客户端回应响应了,这一部分工作在 Raft 核心算法叫 日志复制
因为主节点的责任非常大,所以只有符合条件的节点才可以当选主节点,为了保证集群对外展现的一致性,主节点在处理操作日志时,也一定要谨慎,这部分在 Raft 核心算法叫 安全性
Raft 算法将一致性问题分解为三个子问题:Leader 选举,日志复制,安全性
选主
选主就是在集群中选出一个主节点来负责一些特定的工作,在执行选主过程后,集群中每个节点都会识别出一个特定的唯一的节点作为 leader
节点角色
Leader(领导):负责处理所有客户请求,并将这些请求作为日志项复制到所有 Follower,Leader 定期向所有 Follower 发送心跳信息,以维持自身领导地位,防止 Follower 进入选举过程
Follower(追随者):接受来自 Leader 的日志条目,并在本地应用这些条目,跟随着不直接处理客户请求
Candidate(候选者):当跟随者一段时间没有收到来自 Leader 心跳消息时,它会变得不确定这个Leader是否可用,这个时候,跟随者会转变角色成为 Candidate,并开始尝试通过投票过程成为新的 Leader
任期
Raft 将时间划分成任意长度的任期,每一段任期从一次选举开始,在这个时候会有以一个或者多个 candidate 尝试去成为 leader,在成功完成一次 leaderelection 之后,一个 leader 就会一直管理集群知道任期结束,在某些情况下,一次选举无法选出 leader,这个时候这个任期会以没有 leader 而结束,同时新的任期会很快开始
每个节点都会存储一个当前任期号,该任期号会随着时间单调递增,节点之间通信的时候都会交换当前任期号,如果一个节点的当前任期号比其他节点小,那么它就将自己的任期号更新为较大的那个值,如果一个 candidate 或者 leader 发现自己的任期号过期了,他就会立刻回到 follower 状态,如果一个节点接收了一个带着过期的任期号的请求,那么他就会拒绝这次请求
选举过程
Raft 采用一种心跳机制来触发 leader 选举,当服务器启动的时候都是 follow 状态,如果 follower 在 election timeout 内没有收到来自 leader 的心跳,则会主动发起选举

1.率先超时的节点,自增当前任期号然后切换为 candidate 状态,并投自己一票
2.以并行的方式发送一个 RequestVote RPCs 给集群中的其他服务器节点,希望得到他们的支持
3.等待其他节点的回复

S3节点率先超时,把期号改为 2,切换为 candidate 状态,发起投票请求,并给自己一票
在这个过程中,可能出现三种结果
1.赢得选举,成为 Leader
2.其他节点赢得了选举,他自行切换到 follower
3.一段时间内没有收到投票,保持 candidate 状态,重新发出选举
**第一种情况:**赢得了选举之后,新的 leader 会立刻给所有节点发消息,告诉他们新的领导者,避免他们进入候选者身份

**第二种情况:**S3 和 S1同时发起选举,但 S3 的消息先到达其他节点,这个时候其他节点把自己的票都投给了 S3 则导致没人给S1投票,S3的票数打到一半之后成为 leader,向其他节点发送心跳包,节点 S1 发现自己的 term 低于 S3的,知道有新的leader了,自己转换为 follower

**第三种情况:**所有节点同时超时,发起投票,他们都将票投给了自己,这种情况发生,每个 candidate 都会进行一次超时响应,然后通过自增任期号来开启新一轮的选举,去进行新一轮的投票,如果没有额外的措施,还是所有节点同时超时,这种情况可能会一直持续下去

为了解决上述问题,Raft 采用随机选举超时时间来确保很少产生无结果投票,就算发生了也会很快解决,为了防止选票一开始就被瓜分,选举超时时间是从一个固定的区间中随机选择,这样可以把服务器分散开来以确保在大多数情况下会只有一个服务器先结束运行,这个时候,他就可以赢得选举,并在其他服务器结束超时之前发送心跳
Raft 消息复制
每个仲裁队列都有多个副本,它包含一个主和多个副本,replication factot 为 5 的仲裁队列将会有 1个主节点 和 4个从节点,每个副本都会在不同的 RabbitMQ 节点上
客户端只会与主副本进行交互,主副本再将这些命令复制到从副本,当主副本所在的节点下线,其中一个从副本会被选举成为主副本,继续提供服务

消息复制和主副本选举操作,需要超过半数的副本同意,当生产者发送一条消息,需要超过半数的队列副本将消息写入磁盘以后才会向生产者进行确认,这意味着少部分比较慢的副本不会影响整个队列的性能
仲裁队列的使用
java
@Bean("quorumQUeue")
public Queue quorumQueue(){
return QueueBuilder.durable("quorum.queue").quorum().build();
}

创建后我们来观察管理平台

仲裁队列后面有一个 +2,代表有两个镜像节点
仲裁队列默认的镜像数为 5,一个主节点,4个从节点

当有多个仲裁队列时,主副本和从副本会分布在集群的不同节点上,每个节点可以承载多个主副本和从副本
宕机演示
给quorum1.queue发送消息


我们停掉这个主节点


其他节点这个队列还在

在线的节点就只有俩了
仲裁队列可以极大保障 RabbitMQ 集群对接的高可用
HaProxy 负载均衡
面对大量的业务访问,高并发请求,可以使用高性能的服务器来提升 RabbitMQ 服务的负载能力,当单机容量达到极限时,可以采用集群的策略来对负载能力做进一步提升
但是这其中存在着一个问题,假设我们有三个节点,我们没法保证这三个节点的访问量差不多的,有可能会出现大部分客户端请求都涌向一个节点的情况,那么这个节点的网络负载必然会大大增加,而其他节点又由于没有那么多的负载而造成硬件资源浪费
引入负载均衡之后,各个客户端的连接可以通过负载均衡分摊到集群各个节点中,避免出现上述问题

安装 HAProxy 这里省略了
启动 HAProxy
java
systemctl start haproxy
尝试访问
需要把 RabbitMQ 的 IP 和 port 改为 HAProxy的
修改配置文件
java
spring:
application:
name: rabbitmq-ops-demo
rabbitmq:
addresses: amqp://study:study@47.101.223.145:5670/ops
声明队列
java
@Bean("clusterQueue")
public Queue clusterQueue(){
return QueueBuilder.durable("cluster.queue").quorum().build();
}
发送消息
java
@RequestMapping("/haproxy")
public String haproxy(){
rabbitTemplate.convertAndSend("","cluster.queue","haproxy test ......");
return "消息发送成功";
}
测试


宕机演示
我们停止一个节点
java
rabbitmqctl -n rabbit stop_app
继续发送消息

集群恢复
java
rabbitmqctl -n rabbit start_app

我们发现消息同步到当前节点了
推模式和拉模式
RabbitMQ 支持两种消息传递模式:推模式 和 拉模式
推模式:消息中间件主动将消息推给消费者
拉模式:消费者主动从消息中间件拉取消息
RabbitMQ 支持两种消息传递模式
推模式:对消息的获取更加实时,适合对数据实时性要求比较高的场景
拉模式:消费端可以按照自己的处理速度来消费,避免消息积压,适合需要流量控制或者需要大量计算资源的任务,拉取模式允许消费者在准备好后再请求消息,避免资源浪费
MQ 应用问题
1.幂等性保障
幂等性是某些运算的性质,他们可以被多次应用,但不会改变初始应用的结果
MQ 幂等性
对于 MQ 幂等性是指同一条消息,多次消费,对系统的影响是相同的
一般消息中间件的消息传输保障分为三个层级
1.At most once: 最多一次,消息可能会丢失,但绝不会重复传输
2.At least once:最少一次,消息绝不会丢失,但可能会重复传输
3.Exactly once:恰好一次,每条消息肯定会被传输一次且仅传输一次
导致消息发送重复的场景:
1.发送时消息重复:当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或客户端宕机,导致服务端对客户端应答失败,如果此时Producer意识到消息发送失败并尝试再次发送消息,Consumer 会收到两条内容相同并且 Message ID 也相同的消息
2.投递时消息重复:消息已经投递到 Consumer 并完成业务处理,当客户端给服务器反馈应答时候网络闪断了,为了保证消息至少被消费一次,云消息队列 RabbitMQ 服务器将在网络恢复后再次尝试投递之前已被处理过的消息,Consumer 后续会收到两条内容相同并且 Message ID 也相同的消息
最少一次会造成一个问题,消费端会收到重复的消息,也会对同一条消息进行多次处理,对于一些重要的业务来说是会出现大问题的
解决方案:
全局唯一 ID
1.为每条消息分配一个唯一标识符,比如 UUID 或者 MQ 消息中的唯一ID,但是一定要保证唯一性
2.消费者收到消息之后,先用该 id 判断该消息是否已经消费过,如果消费过了,就丢弃了
3.如果未消费过,消费者开始消费消息,业务处理成功之后,把唯一ID保存起来
业务逻辑处理
通过检查数据库中是否已经存在相关数据的记录
顺序性保障
消息的顺序性是指消费者消费的消息和生产者发送消息的顺序是一致的
比如生产者发送的消息分别是 msg1,msg2,msg3,那么消费者消费消息的顺序也应该是 msg1,msg2,msg3 的顺序进行消费
比如用户信息修改,对同一个用户的同一个资料进行修改,需要保证消息的顺序
RabbitMQ 对于只有一个消费者和一个生产者情况下是可以保证顺序性的,但是真实情况往往是多个生产者同时发消息,无法确定消息到达RabbitMQ的前后顺序,也就无法保障消息的顺序性
有几种常见场景
1.多个消费者:当队列配置了多个消费者时,消息可能会被不同的消费者并行处理,从而导致消息处理的顺序性无法保证
2.网络波动或异常:在消息传递过程中,如果出现网络波动或异常,可能会导致消息确认丢失,从而使得消息被重新入队和重新消费,造成顺序问题
3.消息重试:如果消费者在处理消息后未能及时发送确认或者确认消息在传输过程中丢失,那么MQ可能会认为消息未被消费者成功消费而重试,这也可能导致消息处理的顺序性问题
4.消息路由问题:在复杂的路由场景中,消息可能会根据路由键被发送到不同的队列,从而无法保证全局的顺序性
5.死信队列:消息因为某些原因被放入死信队列,死信队列被消费时,无法保证消息的顺序性和生产者发送消息的顺序一致
顺序性保障方案
消息顺序性保障分为:局部顺序性保证和全局顺序性保证
局部顺序性通常是指在单个队列内部保证消息的顺序,全局顺序性是指多个队列或多个消费者之间保证消息的顺序
1.单队列单消费者
2.分区消费
单个消费者的吞吐太低了,当需要多个消费者以提高处理速度时,可以使用分区消费,把一个队列分割成多个分区,每个分区由一个消费者处理,以此来保证每个分区内消息的顺序性
3.消息确认机制
使用手动消息确认机制,消费者在处理完一条消息后,显式地发送确认,这样RabbitMQ才会移除并继续发送下一条消息
4.业务逻辑控制
消息积压问题
消息积压是指在消息队列中,待处理的消息数量超过了消费者的处理能力,导致消息在队列中不断堆积的现象
发生的原因:
1.消息生产过快
2.消费者处理能力不足
可能原因:
1)消费端业务逻辑复杂,耗时长
2)消费端代码性能低
3)系统资源限制,如 CPU,内存,磁盘 等会限制消费者无法处理的消息
4)异常处理不当,消费者在处理消息时出现异常,导致消息无法被正确处理和确认
3.网络问题:因为网络延迟或不稳定,消费者无法及时接收或确认消息,导致消息积压
4.RabbitMQ 服务器配置偏低
解决方案
1.提高消费者效率
a.增加消费者实例数量
b.优化业务逻辑,比如引入多线程
c.设置 prefetchCount.当一个消费者阻塞时,消息转到其他未阻塞的消费者
d.消息发生异常时,设置合适的重试策略 或 转入到死信队列
2.限制生产者速率
a.流量控制
b.限流
c.设置过期时间
3.资源与配置优化