RabbitMQ可靠性投递生产者确认机制

1、前言

因朋友提到参考源码的需求,已经更新至github 教程测试源码:https://github.com/lhmyy521125/rabbitmq-test

上一篇文章我们详细介绍了Queue队列和Message消息,本章节我们主要聊一聊RabbitMQ使用必须考虑的问题,就是消息可靠性!在生产环境下如何确保消息的可靠性投递,我们首先需要考虑两个问题
1、生产者发送消息,是否发送成功?
2、消费者接收消息如何确认以及拒绝?
当然我们所说的可靠并非一个绝对的概念,因网络、硬件、不可抗因素等;可靠性是一个相对的概念,在条件合理的范围内系统所能确保一切尽可能的趋于完美的消息可靠性;

我们还是用之前的一张图来复习一下从AMQP协议

 

 

 

我们来思考一下需要考虑哪些环节;

  1.Send Massage(消息投递者) 在将消息发送到交换器Exchange的时候,默认RabbitMQ不进行确认投递者是不知道是否投递成功,也就是默认情况下生产者是不知道消息有没有正确地到达服务器,没有到达服务器,如果出现如:网络闪断等因素,则这条消息会无法投递到Exchange


  2.Exchange通过RoutingKey将消息路由至Queue ,这个环节中如果无法路由至Queue队列,如何处理该消息?消息已经路由至Queue队列,却发现没有消费者,又如何处理?,是否也有一样的通知机制告诉我们?


  3.在接收者Receive Message(消息消费者) 在接收到消息后,如何通知RabbitMQ我已经接收到该消息?是否消费者也需要一个确认告知RabbitMQ已经接收到消息?
带着这一系列问题,我们先来看看如何进行保障消息投递的确认;

2、生产者确认

RabbitMQ针对这个问题,提供了两种解决方式;

  事务机制 :RabbitMQ提供了事务机制保证消息投递,RabbitMQ客户端中与事务机制相关的方法有三个: channel.txSelect 和
channel.txCommit 和channel.txRollback


channel.txSelect : 将当前的channel通道设置为事务模式;
channel.txCommit :用于提交事务;
channel.txRollback :用于事务回滚;

 

  但是使用事务会大大降低RabbitMQ的性能,在一些较小的吞吐量情况下,也可以采用事务方式,具体情况视各自的系统来决定,这里仅以一段代码来让大家了解事务的机制

复制代码
try {
    channel.txSelect();
    channel.basicPublish(exchange , routingKey , 
    MessageProperties.PERSISTENT_TEXT_PLAIN , msg.getBytes());
    int result = 1 / 0 ;
    channel.txCommit();
}catch (Exception e) {
    e.printStackTrace();
    channel.txRollback();
}
复制代码

*生产者确认机制 :(Publisher Confirm)机制 **

用网络上的一张图来了解一下确认机制

 

 

 

  1、生产者将Channel设置成Confirm模式,当设置Confirm模式后所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始,ID在同个Channel范围是唯一的),一旦消息被投递到所有匹配的队列之后Broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;

  2、如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出;

  3、RabbitMQ回调消息的deliveryTag包含了确认消息的ID,此外RabbitMQ也可以设置channel.basicAck 方法中的multiple参数,表示到这个序号之前的所有消息都己经得到了处理;稍后介绍handleNack 和 handleAck两个方法我们再举个说明;

  4、confirm的机制是异步的,如果消息成功发送,会返回ack消息供异步处理,如果消息发送失败发生异常,也会返回nack消息,confirm的时间没有明确说明,并且同一个消息只会被confirm一次;

接下来介绍两种confirm方法

1.批量confirm方法 : 每发送一批消息后,调用channel.waitForConfirms方法,等待服务器的确认返回;
先看代码样例,注意看注释

 

复制代码
//开启confirm模式
channel.confirmSelect();
//模拟发送50条消息
for(int i =0;i<1000;i++){
String message = "Hello World RabbitMQ";
//发送消息
channel.basicPublish(EXCHANGE_NAME,"",MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
//每发送2条判断一次是否回复
if(i%2==0){
//waitForConfirms可以换成带有时间参数的方法waitForConfirms(Long mills)指定等待响应时间
if(channel.waitForConfirms()){
System.out.println("Message send success."); 
}
}
}
复制代码

批量的方法从数量级上降低了confirm的性能消耗,提高了效率,但是批量confmn方式的问题在于遇到RabbitMQ服务端返回Basic.Nack 需要重发批量消息而导致的性能降低

 

2.异步confirm方法(推荐) :提供一个回调方法,服务端确认了一条或者多条消息后客户端会回调这个方法进行处理;

依旧还是先看代码:生产者

复制代码
public class ConfirmProducer {

    public static void main(String[] args) throws Exception {
        //1 创建ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.1.28");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("toher");
        connectionFactory.setPassword("toher888");
        //2 创建Connection
        Connection connection = connectionFactory.newConnection();
        //3 创建Channel
        Channel channel = connection.createChannel();
        //4 指定我们的消息投递模式: 消息的确认模式
        channel.confirmSelect();
        //5 声明交换机 以及 路由KEY
        String exchangeName = "test_confirm_exchange";
        String routingKey = "confirm.send";
        //6 发送一条消息
        String msg = "Test Confirm Message";
        channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());
        //7 添加确认监听
        channel.addConfirmListener(new ConfirmListener(){
            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("收到NACK应答");
            }
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("收到ACK应答");
            }
        });
    }

}
复制代码

消费者:

复制代码
public class ConfirmConsumer {

    public static void main(String[] args) throws Exception {
        //1 创建ConnectionFactory
        ConnectionFactory connectionFactory = new ConnectionFactory() ;
        connectionFactory.setHost("192.168.1.28");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("toher");
        connectionFactory.setPassword("toher888");
        //2 创建Connection
        Connection connection = connectionFactory.newConnection();
        //3 创建Channel
        Channel channel = connection.createChannel();
        //4 声明
        String exchangeName = "test_confirm_exchange";
        //指定类型为topic
        String exchangeType = "topic";
        String queueName = "test_confirm_queue";
        //因为*号代表匹配一个单词,生产者中routingKey3将匹配不到
        String routingKey = "confirm.*";
        //表示声明了一个交换机
        channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
        //表示声明了一个队列
        channel.queueDeclare(queueName, true, false, false, null);
        //建立一个绑定关系:
        channel.queueBind(queueName, exchangeName, routingKey);
        //5 创建消费者
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消费端:" + msg);
            }
        };
        //参数:队列名称、是否自动ACK、Consumer
        channel.basicConsume(queueName, true, consumer);
    }

}
复制代码

 

 

 

从上面代码我们可以看到有重写了ConfirmListener两个方法:handleNack 和 handleAck,分别用来处理RabbitMQ 回传的Basic.Nack和Basic.Ack
它们都有两个参数:

1.long deliveryTag : 前面介绍确认消息的ID
2.boolean multiple : multiple 是否批量 如果是True 则将比该deliveryTag小的所有数据都移除 否则只移除该条;
我们简单的用一个数组来说明 [1,2,3,4]存储着4条消息ID , 此时确认消息返回的是 deliveryTag = 3 ,multiple = true那么RabbitMQ会通知我们小于ID3的消息得到确认了,如果multiple = false, 就通知我们ID3的确认了

我们再用修改一下上面的代码看一下

复制代码
//声明一个用来记录消息唯一ID的有序集合SortedSet
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());

//开启confirm模式
channel.confirmSelect();

//异步监听方法 处理ack与nack方法
channel.addConfirmListener(new ConfirmListener() {
    //处理ack multiple 是否批量 如果是批量 则将比该条小的所有数据都移除 否则只移除该条
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        if (multiple) {
            confirmSet.headSet(deliveryTag).clear();
        } else {
            confirmSet.remove(deliveryTag);
        }
    }
    //处理nack 与ack相同
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("There is Nack, SeqNo: " + deliveryTag + ", multiple: " + multiple);
        if (multiple) {
            confirmSet.headSet(deliveryTag).clear();
        } else {
            confirmSet.remove(deliveryTag);
        }
    }
});
复制代码

以上代码按照每一个comfirm的通道维护一个集合,每发送一条数据,集合增加一个元素,每异步响应一条ack或者nack的数据,集合删除一条。SortedSet是一个有序的集合,它的有序是值大小的有序,不是插入时间的有序。JDK中waitForConfirms()方法也是使用了SortedSet集合

3、结语
通过本篇文章相信大家对RabbitMQ生产者确认机制有了一个初步的了解,有的同学可能对什么是ACK,什么是NACK不是很理解,没关系下一节我们将对消费者的确认机制,再来详细介绍,谢谢大家!
————————————————
版权声明:本文为CSDN博主「傲泣龙腾」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lhmyy521125/article/details/88064322

 

 

RabbitMq从入门到精通-ConfirmCallback ReturnCallback 区别及使用

  默认情况下如果一个 Message 被消费者所正确接收则会被从 Queue 中移除

  如果一个 Queue 没被任何消费者订阅,那么这个 Queue 中的消息会被 Cache(缓存),当有消费者订阅时则会立即发送,当 Message 被消费者正确接收时,就会被从 Queue 中移除

 

消息发送确认


发送的消息怎么样才算失败或成功?如何确认?
当消息无法路由到队列时,确认消息路由失败。消息成功路由时,当需要发送的队列都发送成功后,进行确认消息,对于持久化队列意味着写入磁盘,对于镜像队列意味着所有镜像接收成功

ConfirmCallback(只确认是否正确到达 Exchange 中,成功到达则回调)

通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中

复制代码
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback{
 
    @Autowired
    private RabbitTemplate rabbitTemplate;
 
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);            //指定 ConfirmCallback
    }
 
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("消息唯一标识:"+correlationData);
        System.out.println("确认结果:"+ack);
        System.out.println("失败原因:"+cause);
    }
复制代码
  • 还需要在配置文件添加配置
spring:
  rabbitmq:
    publisher-confirms: true 

ReturnCallback(消息失败返回时回调)

  • 通过实现 ReturnCallback 接口,启动消息失败返回,比如路由不到队列时触发回调
复制代码
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ReturnCallback{
 
    @Autowired
    private RabbitTemplate rabbitTemplate;
 
    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnCallback(this);             //指定 ReturnCallback
    }
 
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("消息主体 message : "+message);
        System.out.println("消息主体 message : "+replyCode);
        System.out.println("描述:"+replyText);
        System.out.println("消息使用的交换器 exchange : "+exchange);
        System.out.println("消息使用的路由键 routing : "+routingKey);
    }
}
复制代码
  • 还需要在配置文件添加配置
spring:
  rabbitmq:
    publisher-returns: true 

消息接收确认

消息消费者如何通知 Rabbit 消息消费成功?

1.消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK。
2.自动确认会在消息发送给消费者后立即确认,但存在丢失消息的可能,如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
3.如果消息已经被处理,但后续代码抛出异常,使用 Spring 进行管理的话消费端业务逻辑会进行回滚,这也同样造成了实际意义的消息丢失。
4.如果手动确认则当消费者调用 ack、nack、reject 几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者。

5.如果某个服务忘记 ACK 了,则 RabbitMQ 不会再发送数据给它,因为 RabbitMQ 认为该服务的处理能力有限
6.ACK 机制还可以起到限流作用,比如在接收到某条消息时休眠几秒钟

消息确认模式有:
  AcknowledgeMode.NONE:自动确认
  AcknowledgeMode.AUTO:根据情况确认
  AcknowledgeMode.MANUAL:手动确认
确认消息(局部方法处理消息)

  • 默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual

 

 

  • 或在 RabbitListenerContainerFactory 中进行开启手动 ack
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(new Jackson2JsonMessageConverter());
    factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);             //开启手动 ack
    return factory;
}
  • 确认消息
复制代码
@RabbitHandler
public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
    System.out.println(message);
    try {
        channel.basicAck(tag,false);            // 确认消息
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码

需要注意的 basicAck 方法需要传递两个参数


  deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
  multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息

 

 

 

  • 消费者获取消息时检查到头部包含 error 则 nack 消息
复制代码
@RabbitHandler
public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
    System.out.println(message);
    if (map.get("error")!= null){
        System.out.println("错误的消息");
        try {
            channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
            return;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    try {
        channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //确认消息
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码
  • 也可以拒绝该消息,消息会被丢弃,不会重回队列

 

channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒绝消息

确认消息(全局处理消息)

  • 自动确认涉及到一个问题就是如果在处理消息的时候抛出异常,消息处理失败,但是因为自动确认而导致 Rabbit 将该消息删除了,造成消息丢失
复制代码
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");                 // 监听的队列
    container.setAcknowledgeMode(AcknowledgeMode.NONE);     // NONE 代表自动确认
    container.setMessageListener((MessageListener) message -> {         //消息监听处理
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        //相当于自己的一些消费逻辑抛错误
        throw new NullPointerException("consumer fail");
    });
    return container;
}
复制代码
  • 手动确认消息

 

复制代码
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");              // 监听的队列
    container.setAcknowledgeMode(AcknowledgeMode.MANUAL);        // 手动确认
    container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {      //消息处理
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        if(message.getMessageProperties().getHeaders().get("error") == null){
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("消息已经确认");
        }else {
            //channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("消息拒绝");
        }
 
    });
    return container;
}
复制代码

AcknowledgeMode 除了 NONE 和 MANUAL 之外还有 AUTO ,它会根据方法的执行情况来决定是否确认还是拒绝(是否重新入queue)
  如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认

  当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)

  当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认

  其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过 setDefaultRequeueRejected(默认是true)去设置

复制代码
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");              // 监听的队列
    container.setAcknowledgeMode(AcknowledgeMode.AUTO);     // 根据情况确认消息
    container.setMessageListener((MessageListener) (message) -> {
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        //抛出NullPointerException异常则重新入队列
        //throw new NullPointerException("消息消费失败");
        //当抛出的异常是AmqpRejectAndDontRequeueException异常的时候,则消息会被拒绝,且requeue=false
        //throw new AmqpRejectAndDontRequeueException("消息消费失败");
        //当抛出ImmediateAcknowledgeAmqpException异常,则消费者会被确认
        throw new ImmediateAcknowledgeAmqpException("消息消费失败");
    });
    return container;
}
复制代码

消息可靠总结

  • 持久化
  1. exchange要持久化
  2. queue要持久化
  3. message要持久化
  • 消息确认
  1. 启动消费返回(@ReturnList注解,生产者就可以知道哪些消息没有发出去)
  2. 生产者和Server(broker)之间的消息确认
  3. 消费者和Server(broker)之间的消息确认

————————————————
版权声明:本文为CSDN博主「隔壁老瓦」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wxb880114/article/details/105836274/

posted @   wq9  阅读(666)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
历史上的今天:
2019-08-26 jsonP
点击右上角即可分享
微信分享提示