RabbitMQ高级特性第一篇——消息确认机制
本文代码在RabbitMQ基础篇的基础上进行配置
一、为什么需要消息确认机制
在RabbitMQ基础篇中,我们介绍了使用MQ的优势,但是MQ的引入同时也会给整个项目项目带来一些其他的问题,其中一个问题就是:
如何保证消息的可靠性以及如何确保消息被成功消费了?(常见面试题)。
为了解决这个问题,RabbitMQ提供了消息确认机制。
二、RabbitMQ消息确认机制概述
注意:这是在生产端实现的
RabbitMQ的消息确认机制分为两个部分:
- 消息发送确认
- 消息消费确认
至于为什么会分成这两个部分,我们可以通过分析RabbitMQ消息传递流程得出答案
RabbitMQ组成
各组成部分介绍:
- Producer:消息生产者
- Channel:消息通道
- Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue。
- Exchange:交换机,按照一定规则叫不同的消息分发给不同的消息队列,过滤消息
- Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方。(未被处理的消息都放在这)
- Consumer:消费者,负责处理消息。
RabbitMQ工作流程
- 生产者发送消息到Channel
- Channel将消息发送给Exchange
- Exchange按照指定的匹配规则分发消息
- Queue收到消息后存储起来,慢慢发给Consumer进行处理
在这个过程中,Queue是实际上的消息中转站和消息储存者。那么在这个过程中造成消息丢失的原因就有两个:
- 消息没有成功发送给Queue
- Queue将消息转发给Consumer后,Consumer发生异常没有处理消息,但Queue已经将消息出队了
针对这两个原因,所以RabbitMQ就将消息确认机制分为两部分了
- 消息发送确认:确认消息发送给Queue了,即Queue反馈成功获取了消息
- 消息消费确认:消息发送给Consumer后,由Consumer反馈的消息确认是否出队该消息
通过这两步,就可以实现:确保消息可靠性以及确保消息被成功消费的要求。
三、消息发送确认
消息发送确认通过两步实现
- 确认消息发送到了Exchange(Confirm机制)
- 确认消息发送到了Queue(Return机制)
1. Confirm机制
1. 实现原理
生产者将信道设置成confirm模式,一旦信道进入confirm模式,
所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),
一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),
这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,
那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,
此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
2. 代码实现
创建ConfirmCallback处理器,实现RabbitTemplate.ConfirmCallback接口
/**
* 消息发送到exchange时会触发这个方法
*
* @author 叶子
* @Description ConfirmCallback实现类
* @DevelopmentTools IntelliJ IDEA
* @PackageName icu.yezi.producer.handel
* @Data 2020/11/21 星期六 15:34
*/
public class ConfirmCallbackHandel implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("消息唯一标识:"+correlationData);
System.out.println("确认结果:"+b);
System.out.println("失败原因:"+s+"(如果没有失败则此项为null)");
}
}
创建ReturnCallBack处理类,实现RabbitTemplate.ReturnsCallback接口
/**
* 当消息从交换器发送到对应队列失败时触发
* (比如根据发送消息时指定的routingKey找不到队列时会触发)
*
* @author 叶子
* @Description ReturnsCallback实现类
* @DevelopmentTools IntelliJ IDEA
* @PackageName icu.yezi.producer.handel
* @Data 2020/11/21 星期六 15:38
*/
public class ReturnCallbackHandel implements RabbitTemplate.ReturnsCallback {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("消息内容 body:"+new String(returnedMessage.getMessage().getBody()));
System.out.println("应答码 replyCode: :"+returnedMessage.getReplyCode());
System.out.println("原因描述 replyText:"+returnedMessage.getReplyText());
System.out.println("交换机 exchange:"+returnedMessage.getExchange());
System.out.println("消息使用的路由键 routingKey:"+returnedMessage.getRoutingKey());
}
}
3. 在配置类中进行配置
//初始化加载方法,对RabbitTemplate进行配置
@PostConstruct
void rabbitTemplate(){
//消息发送确认,发送到交换器Exchange后触发回调
rabbitTemplate.setConfirmCallback(new ConfirmCallbackHandel());
//消息发送确认,如果消息从交换器发送到对应队列失败时触发(比如根据发送消息时指定的routingKey找不到队列时会触发)
rabbitTemplate.setReturnCallback(new ReturnCallbackHandel());
}
4. 在yml文件中进行配置
spring:
rabbitmq:
host: localhost
port: 5672
username: 叶子
password: 123456789
virtual-host: /test
publisher-returns: true # 消息发送交换机确认
publisher-confirm-type: correlated # 消息发送队列回调 none代表不开启
四、消息消费确认
注意:这个是在消费端进行配置
1. ACK确认机制
有两种确认模式:
- none 自动确认
- manual 手动确认
注意:一般不采用自动确认模式,
原因:自动确认模式下,当消费者接受消息后,消息队列会自动删除该消息。
此时,如果消费者消费失败,那么该条消息就丢失了
2. 在yml文件中配置ACK为手动模式
spring:
rabbitmq:
host: localhost
port: 5672
username: 叶子
password: 123456789
virtual-host: /test
listener:
simple:
acknowledge-mode: manual # 手动
3. 在配置类中进行配置
/**
* @author 叶子
* @Description RabbitMQ配置类(消费者)
* @DevelopmentTools IntelliJ IDEA
* @PackageName icu.yezi.consumer.config
* @Data 2020/11/25 星期三 11:20
*/
@Configuration
public class RabbitMQConfig {
//RabbitMQ监听容器
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
//设置并发
factory.setConcurrentConsumers(1);
SimpleMessageListenerContainer s=new SimpleMessageListenerContainer();
//最大并发
factory.setMaxConcurrentConsumers(1);
//消息接收——手动确认
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//设置超时
factory.setReceiveTimeout(2000L);
//设置重试间隔
factory.setFailedDeclarationRetryInterval(3000L);
//监听自定义格式转换
//factory.setMessageConverter(jsonMessageConverter);
return factory;
}
}
4. 消息确认常用方法
1. 确认消费的方法
void basicAck(long deliveryTag, boolean multiple) throws IOException
参数说明:
- deliveryTag:消息ID,从1开始
- multiple:是否批量,将一次性ack所有小于deliveryTag的消息。
2. 反馈消息消费失败的方法
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException
basicReject(long deliveryTag, boolean requeue) throws IOException
参数说明:
- deliveryTag:消息ID,从1开始
- multiple:是否批量,将一次性拒绝所有小于deliveryTag的消息。
- requeue:被拒绝的是否重新入队列。
5. 代码编写
/**
* @author 叶子
* @Description RabbitMQ - 消息队列监听类
* @DevelopmentTools IntelliJ IDEA
* @PackageName icu.yezi.consumer.listener
* @Data 2020/11/20 星期五 16:20
*/
@Component
public class RabbitMQListener {
/**
*
* 确认消费成功的方法
* void basicAck(long deliveryTag, boolean multiple) throws IOException
*
* 参数说明:index
* 1. deliveryTag:消息ID,从1开始
* 2. multiple:是否批量,将一次性ack所有小于deliveryTag的消息。
*
* @param message
* @param channel
*/
@RabbitListener(queues = "boot.queue.user")
public void listenerQueueUser(Message message, Channel channel){
System.out.println("普通用户阅读了"+new String(message.getBody()));
System.out.println(message.getMessageProperties().getDeliveryTag());
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 失败确认
* 方法一:void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException
* 方法二:basicReject(long deliveryTag, boolean requeue) throws IOException
* 参数说明:
* 1. deliveryTag:消息ID,从1开始
* 2. multiple:是否批量,将一次性拒绝所有小于deliveryTag的消息。
* 3. requeue:被拒绝的是否重新入队列。
*
* @param message
* @param channel
*/
@RabbitListener(queues = "boot.queue.user.vip")
public void listenerQueueUserVip(Message message,Channel channel){
System.out.println("VIP用户阅读了"+new String(message.getBody()));
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(),true,true);
} catch (IOException e) {
e.printStackTrace();
}
}
}