目录
[1. 幂等性概念](#1. 幂等性概念)
[2. MQ 幂等性介绍](#2. MQ 幂等性介绍)
[3. 幂等性的解决方法](#3. 幂等性的解决方法)
[1. 顺序性介绍](#1. 顺序性介绍)
[2. 解决方案](#2. 解决方案)
[1. 原因分析](#1. 原因分析)
[2. 解决方案](#2. 解决方案)
前言
本文介绍 RabbitMQ 在实际应用中,可能出现的一些应用问题,如幂等性保障,顺序性保障,消息积压等;以及针对这些问题,一些典型的解决方案。
一、幂等性保障
1. 幂等性概念
在计算机领域中,幂等性指的是同一个接口,请求或者操作,发起一次或重复发起 N 次,对系统资源或者数据状态,造成的影响完全相同,不会产生脏数据,重复扣款或者重复创建等问题。
核心本质:重复执行,结果一致。
技术价值:解决消息重复,重试带来的数据错乱问题;
MQ 消息消费幂等:同一条消息重复消费 N 次,业务只生效一次;
2. MQ 幂等性介绍
通常消息中间件的消息传输保障分为 3 个等级:
- at most once:最多一次,消息有可能会丢失,但绝不可能重复传输;
- at least once:最少一次,消息绝不会丢失,但是可能重复传输;
- exactly once: 恰好一次,每条消息只会被传输一次;
RabbitMQ 支持最多一次和最少一次;
通常业务可靠性要求比较高时,会使用"最少一次",防止消息丢失。但是最少一次,就会存在消息重复传输的问题;
如果不对重复消息进行幂等性处理,有可能会造成严重的问题。比如用户付款后,由于网络问题,业务没有进行处理,用户重复点击后,再次进行了扣款。
3. 幂等性的解决方法
1. 全局唯一 ID
1)为每条消息分配一个唯一 ID,比如 UUID 或者 MQ 中的 deliveryId;
2)消费者收到消息后,判断该 Id 是否已经消费过;
3)如果未消费过,则开始消费消息,并保存唯一 ID;如果消费过,则放弃处理;
2. 业务逻辑判断
处理消息之前,先检查相关业务状态,确保对应的操作尚未执行,再进行消息的处理;
二、顺序性保障
1. 顺序性介绍
消息的顺序性指的是:消费者消费消息的顺序和生产者发送消息的顺序是一致的;
有些业务场景,消费者消费消息是不需要保障顺序的;比如不同用户的操作,即使顺序不一致,也不影响业务处理的结果;
有些业务场景,可能存在多个消息顺序处理的场景;比如同一个用户的操作,顺序不一致,可能会影响结果;
RabbitMQ 如果一个生产者只有一个消费者,借助队列先进先出的特性,可以保障消息的顺序;
打破 RabbitMQ 顺序性的场景:
-
- 多个消费者:多个消费者处理消息的速度不相同,不能保证消息的顺序性;
-
- 网络波动:消息确认丢失,导致消息重新入队,重新消费;
-
- 消息重试:消费者处理消息后没有及时确认,导致消息重试;
-
- 消息路由:消息根据路由键不同,被路由到不同的队列;
-
- 死信队列:消息消费时出现异常,消息被路由到死信队列;
2. 解决方案
消息的顺序保障分为:局部消息顺序保障和全局消息顺序保障;
局部顺序保障:在单个队列中保证顺序;
全局顺序保障:在多个队列和对各消费者之间保障顺序;
常用策略:
-
单队列单消费者:利用队列的先进先出特性,保证消息的顺序;
-
分区消费:单个消费者的吞吐太低,当需要多个消费者提高处理速度时,可以使用分区消费,把一个队列分割成多个分区:每个分区由一个消费者进行处理,保证分区内消息的顺序性;
-
消息确认:消息确认,消费者按照顺序进行确认,每处理完一个消息,按照消息顺序发送确认;
-
业务逻辑控制:消息可能会乱序达到,由业务逻辑实现顺序控制;
三、消息积压
消息积压指的是,消费者消费消息的速度小于生产者生产消息的速度,导致消息在队列中越来越多;
1. 原因分析
消息积压通常有一下几种原因:
-
消息生产过快:消息生产的速度超过了消费的速度;
-
消费者能力不足:消费者能力不足,如消费逻辑复杂耗时,消费代码性能不足,系统资源限制,异常处理不当等;
-
网络问题:由于网络问题,消费者无法及时接收或者确认消息;
-
RabbitMQ 服务器配置偏低;
2. 解决方案
出现消息积压的情况,首先要分析消息积压的原因,根据原因调整策略;
1. 提高消费者效率
1)增加消费者数量
2)优化业务逻辑,使用多线程的解决方案;
3)设置 prefetchCount,当一个消费者阻塞,分发消息到另一个消费者;
4)消息消费异常时,重试或者转发到死信队列;
2. 限制生产者速率
1)在消息生产者中实现流量控制;
2)限流,给生产者发送速率设置上限;
3)设置过期时间:过期消息进入死信队列;
3. 升级 RabbitMQ 服务器的配置
四、仲裁队列
RabbitMQ 的仲裁队列是一种基于 Raft 一致性算法实现的持久化,复制的 FIFO 队列;
仲裁队列提供队列的复制能力,保障数据的高可用和安全性;
仲裁队列可以在 RabbitMQ 节点之间进行队列数据的复制,从而实现一个节点宕机,其它节点依然可以提供服务;
Raft 协议下的消息复制:
每个仲裁队列都有多个副本,包含一个主副本和多个从副本,每个副本都在不同的 RabbitMQ 节点上;
客户端只会与主副本进行交互,主副本再将这些命令复制到从副本。当主副本所在的节点下线,另外一个从副本就会被选举为主副本,继续提供服务;
消息复制和主副本选举的操作,需要半数以上的副本同意,当生产者发送一条消息,需超过半数的队列副本,将消息写入磁盘以后,才会向生产者进行确认,这意味着少部分比较慢的副本不影响整个队列的性能;
交换机,队列及其绑定关系:
java
public class Constants {
public static final String QUORUM_QUEUE = "quorum.queue";
public static final String QUORUM_EXCHANGE = "quorum.exchange";
public static final String CLUSTER_QUEUE = "cluster.queue";
public static final String CLUSTER_EXCHANGE = "cluster.exchange";
}
java
@Configuration
public class RabbitMQConfig {
/**
* 仲裁队列
* @return
*/
@Bean("quorumQueue")
public Queue quorumQueue(){
return QueueBuilder.durable(Constants.QUORUM_QUEUE).quorum().build();
}
@Bean("quorumExchange")
public DirectExchange quorumExchange(){
return ExchangeBuilder.directExchange(Constants.QUORUM_EXCHANGE).build();
}
@Bean("quorumBinding")
public Binding quorumBinding(@Qualifier("quorumQueue") Queue queue,
@Qualifier("quorumExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("quorum");
}
}
生产者:
java
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("quorum")
public String quorum(){
String message = "hello, quorum test...";
rabbitTemplate.convertAndSend(Constants.QUORUM_EXCHANGE, "quorum", message);
return "消息发送成功!";
}
}
运行结果:

可以看到队列后面有两个镜像节点,都保存了仲裁队列的从副本;
停止主节点,观察仲裁队列:


可以看到队列中的消息仍然存在,没有丢失。原因是从副本重新选举为主副本。
当有多个仲裁队列时,主副本和从副本会分布在集群的不同节点上,每个节点可以承载多个主副本和从副本。
总结
本文仅仅简单介绍了 RabbitMQ 在实际应用中可能存在的一些问题,以及这些问题的常见的解决方案。