可靠性传输
传输中可能遇到的问题

- 生产者将消息发送到MQ失败
- 可能原因:网络问题
- 解决方法:消息确认机制-confirm模式
- 消息在交换机中无法路由到指定队列
- 可能原因:代码或者配置层错误,导致路由失败
- 解决方法:消息确认机制-return模式
- 消息队列自身数据丢失
- 可能原因:消息到达RabbitMQ,服务器宕机导致消息丢失
- 解决方法:开启交换机持久化,队列持久化以及消息持久化
- 消费者异常,导致信息丢失(消息确认机制)
- 可能原因:消息未到达消费者或者消费者宕机、逻辑有问题等
- 解决方法:消息应答机制,重试机制
消息确认
概念
生产者发送消息到达消费端之后有两种情况,消息处理成功或者异常。为了保障消息从队列可靠地到达消费者,RabbitMQ提供了消息确认机制。通过basicConsume()构造方法指定的autoAck 参数设置
-
自动确认,autoAck为true:
当消息从RabbitMQ发送出去之后,RabbitMQ自动确认,然后将这条消息删除,无论消费者是否真的接收到消息
-
手动确认,autoAck为false:
当消费者调用Basic.Ack命令,RabbitMQ才确认消息已被消费,然后将消息删除。如果消息此时已经发送给消费者,但是消费者在发送确认消息之前断开连接,那么RabbitMQ会重新将这条消息放到消息队列中
手动确认方法
基于原生API
- 肯定确认:
channel.basicAck(long deliveryTag, boolean multiple)- deliveryTag:消息的唯一标识,一个单调递增的64位长整型。由每个channel独立维护,并保证在每个channel中是唯一的,所以必须使用对应的channel来确认消息
- multiple:是否批量确认,如果为true,那么会一次性确认小于等于deliveryTag的所有消息。也就是确认本条以及之前的所有消息。
- 单条否定确认:
channel.basicReject(long deliveryTag, boolean requeue)- requeue:如果为true,那么RabbitMQ会将这条消息重新入队
- 多条否定确认:
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue)- multiple为true,表示批量拒绝
基于SpringBoot实现消息确认
Spring-AMQP对消息确认机制提供了三种策略(通过配置文件配置):
yaml
spring
rabbitmq:
listener:
simple:
acknowledge-mode: manual / auto / none
- AcknowledgeMode.NONE:消息一旦发送给消费者,无论消费者是否成功处理了该消息,RabbitMQ都会自动确认,并将消息移除
- AcknowledgeMode.AUTO(默认):在消费者处理消息成功时自动确认消息,如果消息处理的过程中抛出异常,则不会确认消息,并自动进行重试
- AcknowledgeMode.MANUAL:与RabbitMQ原生API的手动确认模式相同
持久性
交换机持久化
通过在声明交换机时将durable参数置为true实现,重启RabbitMQ时不需要重新建立交换机,一旦建立持续存在。如果设置为false,那么交换机仅保存在内存中,一旦RabbitMQ重启,交换机就消失了
java
ExchangeBuilder.topicExchange(name).durable(true).build()
队列持久化
与交换机持久化类似,在声明队列时将durable参数置为true实现,只能保证队列不丢失,不能保证队列内部的消息不丢失
Java
QueueBuilder.durable(name).build();
QueueBuilder.nonDurable(name).build()
消息持久化
想要实现消息持久化,需要同时实现队列持久化,以及消息持久化
java
//原生api
channel.basicPublish(EXCHANGE_NAME,QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,body);
//spring
// 创建一个Message对象,设置为持久化
Message messageObject = new Message(body, new
MessageProperties());
messageObject.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSIS
TENT);
rabbitTemplate.convertAndSend(EXCHANGE_NAME,routingKey,
messageObject);
如果将所有消息都设置为持久化,会严重影响RabbitMQ的性能
在持久化消息正确存入RabbitMQ之后,还需要一段时间才能存入磁盘,RabbitMQ不会为每条消息都调用内核的fsync方法进行存盘,而是先保存到缓存中,每隔一段时间集中落盘一次。如果在这段时间RabbitMQ节点发生宕机,那么消息就会丢失。可以引入仲裁队列来解决这一问题
发送方确认
用以判断消息是否到达RabbitMQ服务器
confirm模式
Producer在发送消息的时候,对发送端设置一个ConfirmCallback的监听,如果交换机成功收到,那么ACK为true,反之ACK为false
yaml
publisher-confirm-type: correlated
因为每个RabbitTemplate 实例仅允许设置一个ConfirmCallback,所以需要全局设置RabbitTemplate。这样当注入RabbitTemplate时,只会执行一次setConfirmCallback
java
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate=new RabbitTemplate(connectionFactory);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause) {
if(ack){
//成功接收
}else{
//未成功接收
}
}
});
return rabbitTemplate;
}
return模式
消息到达交换机之后,会根据路由规则匹配对应的队列,如果这时有一条消息无法被任何队列消费,可以选择把消息退回给发送者。可以设置一个回调方法,对退回的消息进行处理
java
//此方法与setConfirmCallback方法类似,都只能配置一次
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback(){
@Override
public void returnedMessage(@NonNull ReturnedMessage returned) {
//处理逻辑
}
});
ReturnedMessage包含以下属性:
java
public class ReturnedMessage {
//返回的消息对象,包含了消息体和消息属性
private final Message message;
//由Broker提供的回复码, 表示消息无法路由的原因. 通常是一个数字代码,每个数字代表不同的含义.
private final int replyCode;
//一个文本字符串, 提供了无法路由消息的额外信息或错误描述.
private final String replyText;
//消息被发送到的交换机名称
private final String exchange;
//消息的路由键
private final String routingKey;
}
重试机制
允许消费者处理消息失败后可以重试,用以解决网络故障,服务不可用,资源不足等问题。当重试次数耗尽,可以将其转发到死信队列,做进一步处理
自动确认
配置:
yaml
spring
rabbitmq:
listener:
simple:
acknowledge-mode: auto #消息自动确认机制
retry:
enabled: true
initial-interval: 5000ms #初始失败等待时长
max-retries: 5 #最大重试次数,包括第一次
手动确认
否定确认时,配置消息重新入队
java
try{
消息处理逻辑
...
channel.basicAck(tag,是否批量确认);
}catch(Exception e){
channel.basicNack(tag,是否批量确认,是否重试);
}
传输保障的三个层级
-
At most once:最多一次. 消息可能会丢失,但绝不会重复传输
不设置任何消息放丢失的措施,生产者仅发送一次消息
-
At least once:最少一次. 消息绝不会丢失,但可能会重复传输
设置消息确认以及重试机制
-
Exactly once:恰好一次. 每条消息肯定会被传输一次且仅传输一次
RabbitMQ不支持
TTL
消息过期时间,当消息到达过期时间之后,还没有被消费,就会被删除
-
设置队列的TTL,队列中所有消息TTL都相同。队列中过期的消息一定在头部,所以MQ只需要定期扫描队列头,来删除过期消息
javaQueueBuilder.durable(QUEUE_NAME).ttl().build() -
设置消息的TTL,为消息设置单独的TTL,延迟删除。队列中每个消息的过期时间不同,RabbitMQ只会监听队首的消息(先进先出),是否到达过期时间。
javaMessagePostProcessor messagePostProcessor= message -> { message.getMessageProperties().setExpiration("20000"); //单位ms return message; }; rabbitTemplate.convertAndSend(EXCHANGE_NAME,routingKey,body,messagePostProcessor); //简写: rabbitTemplate.convertAndSend(EXCHANGE_NAME,routingKey,body,message -> { message.getMessageProperties().setExpiration(""); //单位ms return message; }); -
如果同时设置队列的TTL和消息的TTL,那么以短的TTL为准
死信队列
死信:因为种种原因,无法被消费的消息。当一个消息变成死信之后,它被重新投递到另一个交换器中(死信交换机)与死信交换机绑定的队列,就是死信队列。死信最终会被投递到死信队列中。
成为死信的可能原因:
- 消息被拒绝,并且设为不重新投递,basicNack
- 消息过期
- 队列达到最大长度
执行流程
- 将消息转发到死信交换机,并指定RoutingKey
- 死信交换机将消息转发到与之绑定的死信队列

配置死信队列
普通队列与死信交换机绑定
java
QueueBuilder.durable(NORMAL_QUEUE)
.deadLetterExchange(DLX_EXCHANGE_NAME)//为普通队列设置死信交换机
.deadLetterRoutingKey("")//转发的消息携带的routingKey,要与死 信队列与死信交换机的BindingKey相同
.maxLength() //队列最大长度
.build();
//死信队列与死信交换机绑定,与普通队列与交换机绑定相同
如果不显示指定deadLetterRoutingKey,会沿用消息最初发送到正常交换机时携带的RoutingKey
应用
- 用户支付订单后,支付系统给订单系统返回当前订单的支付状态,为了保证消息不丢失,就需要使用死信队列机制。由其他消费者监听死信队列,并对其进行处理(比如发送工单,并进行人工确认)
- 消息丢弃:直接丢弃无法处理的消息,避免占用系统资源
- 日志收集:将死信消息作为日志收集起来,便于后续分析和问题定位
延迟队列
消息被发送后,不让消费者立刻消费消息,而是等待一定时间之后消费者才能拿到这个消息进行消费
实现
TTL+死信队列实现
将TTL设置为需要延迟的时间,当过期时间到达后,消息转发到死信队列。消费者负责监听死信队列,以模拟延迟队列的功能
出现的问题:
当设置消息的TTL时,消息过期之后,不一定马上丢弃,因为RabbitMQ只会检查队首元素是否过期。所以当先发送消息的TTL大于后发送消息的TTL时,后发的消息也不会优先执行,而是两者
延迟队列插件
https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq
插件安装:
-
下载插件
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
-
安装插件
复制到/usr/lib/rabbitmq/lib/rabbitmq_server-3.12.1/plugins或者/usr/lib/rabbitmq/plugins(需要自行创建)目录下
-
启动插件
#查看插件列表 rabbitmq-plugins list #启动插件 rabbitmq-plugins enable rabbitmq_delayed_message_exchange #重启服务 service rabbitmq-server restart
插件使用:
当到达时间之后,消息才会被路由到延迟队列供消费者使用
-
声明延迟交换机
javaExchangeBuilder.directExchange(name).durable(true) .delayed().build(); -
发送消息
javarabbitTemplate.convertAndSend("exchange",RoutingKey,body,message -> { message.getMessageProperties().setDelayLong(); //单位ms return message; });
两者对比
- 基于死信队列
- 优点:灵活,不需要额外的插件
- 缺点:1)消息顺序问题 2)需要额外的逻辑处理死信队列的消息
- 基于插件
- 优点:1)可以通过插件直接创建延迟队列 2)避免DLX的时序问题
- 缺点:需要依赖对应的插件,只适用于特定版本
应用场景
- 智能家居:用户通过手机远程遥控智能设备在指定时间进行工作。这时候可以将指令发送到延迟队列,到达时间后再发送给智能设备
- 订单十分钟内未支付自动取消,或者用户发起退款后,24H商家未处理则自动处理退款
事务
RabbitMQ事务允许开发者确保消息的发送和接收是原子性的, 要么全部成功, 要么全部失败。使用方法很简单,只需要在需要事务的方法上加上@Transactional注解
-
配置RabbitTemplate
java@Bean("transRabbitTemplate") public RabbitTemplate transRabbitTemplate(ConnectionFactory connectionFactory){ RabbitTemplate rabbitTemplate=new RabbitTemplate(connectionFactory); rabbitTemplate.setChannelTransacted(true); return rabbitTemplate } -
配置事务管理器
java@Bean public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory){ return new RabbitTransactionManager(connectionFactory); } -
在需要事务的方法上加上
@Transactional注解
RabbitMQ 的原生事务是同步阻塞式的,会极大地拉低吞吐量
消息分发
默认情况下,RabbitMQ采用轮询的方式进行消息分发。但是这种方式并不合理,如果某些消费者消费的快,有些消费者消费的慢,单纯的轮询分发就会导致消息积压以及消费效率下降
限流
控制消费者一次仅拉取n个请求
如下场景:
订单订单系统每秒最多处理5000请求,在秒杀节点,订单量瞬间增多。假设增加到每秒10000个请求,如果这些请求全部通过MQ发送给订单系统,就会将其压垮。这时就可以配置订单系统一次只拉取5000个请求
配置:
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual #必须设置为手动应答
prefetch: # 一次能拉取的请求个数
负载均衡
用来解决多个消费者处理消息的速度不统一导致的消息积压问题。设置prefetch为1。每次只给消费者发送一条消息,当消费者ack之后,再发送消息
yaml
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual #必须设置为手动应答
prefetch: # 一次能拉取的请求个数,10~50
推拉模式
推模式
消息中间件主动将消息推送给消费者,RabbitMQ主要是基于推模式工作,消费者订阅消息队列,由消息队列将消息路由到消费者
对于消息的获取更加实时,适合对数据实时性比较高,比如数据处理等
拉模式
消费者主动从消息中间件拉取信息
基于Spring实现:
java
Message message=rabbitTemplate.receive(QUEUE_NAME);
基于RabbitMQ SDK实现
java
GetResponse getResponse = channel.basicGet(QUEUE_NAME, autoACK);
消费端可以按照自己的处理速度来消费,避免消息积压,适合需要流量控制,或者需要大量计算资源的任务。拉模式允许消费者在准备好后再请求消息,避免资源浪费
应用问题
幂等性保证
幂等性
幂等性指可以被多次应用,但是不改变初始应用的结果(取绝对值操作)
-
应用程序的幂等性:
对系统进行重复调用(相同参数),不论请求多少次,这些请求对系统的影响都是相同的。比如数据库的select语句,就满足幂等性。++i自增操作就不满足幂等性
-
MQ幂等性:
同一条消息,多次消费,对系统的影响时相同的
解决方案
-
业务逻辑判断:
-
消息处理之前,先检查相关业务的状态,确保消息对应的操作尚未执行
-
使用乐观锁来保障数据库中的数据仅被修改一次,用于数据更新以及状态扭转
在表中指定一个版本号字段,消费者收到消息先去数据库查询当前记录的状态以及版本号,消费者处理完业务逻辑后,编写update语句,必须将刚才查到的版本号作为where条件之一,并在更新成功后加一
-
-
全局唯一ID:
-
为每条消息分配一个唯一标识符,单机环境下可以使用UUID,集群环境下可以使用雪花算法。
-
消费者收到消息,先判断这条消息是否已经消费过,如果已经消费过则放弃处理
使用Redis的setnx命令(setnx ID 1),尝试写入唯一业务号,返回1则表示这条消息没有被处理
-
如果未消费过,消费者开始处理消息,业务处理成功后,将唯一的ID保存到数据库
-
顺序性保证
顺序性保证指消费者消费消息的顺序与生产者发送消息的顺序是一致的
打破顺序性的情况
- 多个消费者:消息被多个消费者并行处理,导致消息处理的顺序性无法保证
- 网络波动或异常:在消息传递的过程中,由于网络的原因导致ACK丢失,从而使消息被重新入队或者重新消费
- 消息路由问题:复杂的路由场景中,消息会根据路由键被发送到不同的队列,从而无法保障全局的顺序性
- 死信队列:消息因为某些原因被放入死信队列,死信队列被消费时,无法保证消费的顺序和生产者发送消息的顺序一致
- ...
顺序性保证
局部性顺序保证:单个队列内部保证消息处理的顺序
全局顺序性保证:多个队列多个消费者之间保证消息的顺序
-
单队列单消费者
使用单个队列,并由单个消费者进行处理
-
分区消费
把一个队列分割成多个分区,每个分区由一个消费者处理,保证每个分区内部消息的顺序性
比如:处理消息订单的队列,就可以将消息ID通过某种算法路由到对应的消费者,每个订单的消息扭转都由同一个消费者进行
-
消息确认机制
使用收到消息确认机制,消费者在处理完一条消息之后,显示的发送ACK,RabbitMQ接收到ACK才移除并发送下一条消息
-
业务逻辑控制
在业务层面实现顺序控制,比如生产者发送消息时带上时间戳。消费者消费时进行判断,如果消息的时间戳晚于数据库保存的时间戳,那么更新数据库以及时间戳,反之不做任何操作。
消息积压
生产者生产的数量超过了消费者的处理能力,导致消息在队列中不断堆积
原因
- 消息生产过快
- 消费者处理能力不足
- 消费端业务复杂,耗时长,代码性能低
- 系统资源限制
- 异常处理处理不当,消费者在处理消息时出现异常,导致消息无法被正确处理和确认
- 网络问题:网络抖动导致消息无法投递到消费者
- RabbitMQ服务器配置偏低
解决方案
- 提高消费者效率
- 增加消费者实例数量,新增机器
- 优化业务逻辑,使用多线程处理业务
- 设置消费者一次能拉取的消息个数
- 当消息处理抛出异常时,使用合适的重试机制或者将消息转发到死信队列
- 限制生产者速率
- 流量控制:根据消费者处理能力动态调整发送速率
- 限流:使用限流工具,为消息发送速率设置上限
- 设置过期时间:如果消息过期未消费,转入死信队列,来避免消息丢失并减少主队列的压力
- 资源与配置优化:升级服务器硬件,调整RabbitMQ配置参数