一、前言

RabbitMq提供了消息确认机制,主要分为生产者端发送消息确认和消费者端的消费消息确认。
1、生产者端发送消息确认又分为Confirm 消息确认机制和Return 消息机制

2、消费者端消息接收确认采用的是ack模式

  • AcknowledgeMode.NONE :自动确认
  • AcknowledgeMode.AUTO:根据情况确认
    如果消息成功被消费(成功的意思是在消费的过程中没有抛出异常),则自动确认
    当抛出 AmqpRejectAndDontRequeueException 异常的时候,则消息会被拒绝,且 requeue = false(不重新入队列)
    当抛出 ImmediateAcknowledgeAmqpException 异常,则消费者会被确认
    其他的异常,则消息会被拒绝,且 requeue = true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过 setDefaultRequeueRejected(默认是true)去设置
  • AcknowledgeMode.MANUAL:手动确认

rabbitmq python 消息设置为手动确认_spring

二、生产者端消息确认机制

生产者端发送消息确认又分为Confirm 消息确认机制和Return 消息机制

2.1 引入 rabbitmq 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springboot-rabbitmq-fanout-producer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
    </dependencies>
</project>

2.2 application.yml

# 服务端口
server:
  port: 8080
# 配置rabbitmq服务
spring:
  rabbitmq:
    username: guest
    password: guest
    virtual-host: /

    #由于开启了集群,不再建议使用此方式配置服务地址,若集群含有多个IP地址,不方便指定
#    host: 192.168.229.128
#    port: 5672
    #集群地址配置:指定client连接到的server的地址,多个以逗号分隔(优先取addresses,然后再取host)
    addresses: 192.168.229.128:5672
    # 开启发送确认(有些博文写的是publisher-confirms: true,但其实此方式已经被遗弃,替换为publisher-confirm-type)
    publisher-confirm-type: correlated
#    # 开启发送失败退回(例如:消息有没有找到合适的队列)
    publisher-returns: true

springboot.rabbitmq.publisher-confirm 新版本已被弃用,现在使用 spring.rabbitmq.publisher-confirm-type = correlated 实现相同效果

在springboot2.2.0.RELEASE版本之前是amqp正式支持的属性,用来配置消息发送到交换器之后是否触发回调方法,在2.2.0及之后该属性过期使用spring.rabbitmq.publisher-confirm-type属性配置代替,用来配置更多的确认类型:

  • NONE值是禁用发布确认模式,是默认值
  • CORRELATED值是发布消息成功到交换器后会触发回调方法,如1示例
  • SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用,waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;

2.3 Exchange 和 Queue

package com.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * @Author : JCccc
 * @CreateTime : 2019/9/3
 * @Description :
 **/
@Configuration
public class DirectRabbitConfig {

    //创建队列
    @Bean
    public Queue directEmailQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //一般设置一下队列的持久化就好,其余两个就是默认false
        return new Queue("email.direct.queue", true);
    }
    @Bean
    public Queue directSmsQueue() {
        return new Queue("sms.direct.queue", true);
    }
    @Bean
    public Queue directWeixinQueue() {
        return new Queue("weixin.direct.queue", true);
    }


    //创建交换机
    @Bean
    public DirectExchange directOrderExchange() {
        return new DirectExchange("direct_order_exchange", true, false);
    }


    //绑定关系
    @Bean
    public Binding directEmailBinding() {
        return BindingBuilder.bind(directEmailQueue()).to(directOrderExchange()).with("email");
    }
    @Bean
    public Binding directSmsBinding() {
        return BindingBuilder.bind(directSmsQueue()).to(directOrderExchange()).with("sms");
    }
    @Bean
    public Binding directWeixinBinding() {
        return BindingBuilder.bind(directWeixinQueue()).to(directOrderExchange()).with("weixin");
    }
}

2.4 消息发送确认

发送消息确认:用来确认生产者 producer 将消息发送到 broker ,broker 上的交换机 exchange 再投递给队列 queue的过程中,消息是否成功投递。

消息从 producer 到 rabbitmq broker有一个 confirmCallback 确认模式。(无论成功失败都有返回)

消息从 exchange 到 queue 投递失败有一个 returnCallback 退回模式。(失败时才会有返回)

我们可以利用这两个Callback来确保消的100%送达。

2.4.1 ConfirmCallback确认模式

消息只要被 rabbitmq broker 接收到就会触发 confirmCallback 回调 。

package com.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {
    
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        log.info("消息标识:" + correlationData.toString());
        log.info("发送成功确认:"+ack);
        log.info("错误原因:"+cause);
    }
}

实现接口 ConfirmCallback ,重写其confirm()方法,方法内有三个参数correlationData、ack、cause。

  • correlationData:对象内部只有一个 id 属性,用来表示当前消息的唯一性。
  • ack:消息投递到broker 的状态,true表示成功。
  • cause:表示投递失败的原因。

但消息被 broker 接收到只能表示已经到达 MQ服务器,并不能保证消息一定会被投递到目标 queue 里。所以接下来需要用到 returnCallback 。

2.4.2 ReturnCallback 退回模式

如果消息未能投递到目标 queue 里将触发回调 returnCallback ,一旦向 queue 投递消息未成功,这里一般会记录下当前消息的详细投递数据,方便后续做重发或者补偿等操作。

package com.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息:"+message.toString());
        log.info("返回码:"+replyCode);
        log.info("返回描述:"+replyText);
        log.info("交换机:"+exchange);
        log.info("路由key:"+routingKey);
    }
}

实现接口ReturnCallback,重写 returnedMessage() 方法,方法有五个参数message(消息体)、replyCode(响应code)、replyText(响应内容)、exchange(交换机)、routingKey(队列)。

2.4.3 测试结果

@Test
    public void contextLoads7() throws Exception {
        myService.sendMessage("direct_order_exchange","email","我是发送者");

        Thread.sleep(5000);
    }

为何要沉睡5秒呢?因为使用这种单元测试方式,程序一运行完就会立即关闭应用,而回调函数的执行会有延迟,故为了保证能收到消息确认而设置了沉睡一下再关闭程序。

成功测试:

rabbitmq python 消息设置为手动确认_java_02

失败测试:

rabbitmq python 消息设置为手动确认_spring boot_03


注意:因为投递到broker 已经成功了的,因此错误信息总为null,这里就不演示了。

三、消费者端消息确认机制

消费者端消息接收确认采用的是ack模式。

1、什么是消息确认ACK。

答:如果在处理消息的过程中,消费者的服务器在处理消息的时候出现异常,那么可能这条正在处理的消息就没有完成消息消费,数据就会丢失。为了确保数据不会丢失,RabbitMQ支持消息确定-ACK。

2、ACK的消息确认机制。

答:ACK机制是消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除。

如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中。

如果在集群的情况下,RabbitMQ会立即将这个消息推送给这个在线的其他消费者。这种机制保证了在消费者服务端故障的时候,不丢失任何消息和任务。消息永远不会从RabbitMQ中删除,只有当消费者正确发送ACK反馈,RabbitMQ确认收到后,消息才会从RabbitMQ服务器的数据中删除。消息的ACK确认机制默认是打开的。

3、ACK机制的开发注意事项。

答:如果忘记了ACK,那么后果很严重。当Consumer退出时候,Message会一直重新分发。然后RabbitMQ会占用越来越多的内容,由于RabbitMQ会长时间运行,因此这个"内存泄漏"是致命的。

3.1 引入依赖

3.2 application.yml

# 服务端口
server:
  port: 8081
# 配置rabbitmq服务
spring:
  rabbitmq:
    username: guest
    password: guest
    virtual-host: /
    addresses: 192.168.229.128:5672
#    host: 192.168.229.128
#    port: 5672

3.3 Exchange 和 Queue

若生产者端未配置,则需要配置;这里生产者已经配置了,故这里无需配置。

3.4 消息接收确认

3.4.1 默认ack

代码:

package com.service.direct;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;

// bindings其实就是用来确定队列和交换机绑定关系
@RabbitListener(queues = {"email.direct.queue"})
@Component
public class DirectEmailService {
    @RabbitHandler
    public void messagerevice(String msg, Channel channel, Message message){
        // 此处省略发邮件的逻辑
        System.out.println("email-------------->" + message);
//        int i = 10/0;
    }
}

rabbitmq默认的是自动ack,无需添加其他配置。

自动ack的特性:
消费者自行监控队列,若有队列存在未消费的消息,则进行消费。
若正常消费成功了,则会自动返回确认ack给队列,队列收到后即可将消息移除。
若消费过程中出现异常,则会触发重试消费,若一直报错则会出现死循环,因为消费方一直没有返回ACK确认,RabbitMQ认为消息未进行正常的消费,会将消息再次放入到队列中,再次让你消费,但是还是没有返回ACK确认,依次循环,形成了死循环。

问题:使用自动ack时,如何解决出现死循环的情况?
方案一: 异常捕获
如果消息发送的时候,程序出现异常,后果很严重的,会导致内存泄漏的,所以在程序处理中可以进行异常捕获,保证消费者的程序正常执行,这里不进行介绍了

方案二:控制重试次数
修改配置文件

spring:
  rabbitmq:
    username: guest
    password: guest
    virtual-host: /
    addresses: 192.168.229.128:5672
#    host: 192.168.229.128
#    port: 5672

    listener:
      simple:
#        acknowledge-mode: manual # 设置消费端手动 ack
        retry:
          enabled: true # 是否支持重试
          max-attempts: 10 #最大重试次数,默认为3次
          initial-interval: 2000ms #重试时间间隔

测试后,可发现重试了10次就停止了,但队列的消息也被移除掉了,会造成消息丢失。

方案三:控制重试次数+死信队列
加上死信队列,可以实现重试次数结束后,队列会将消息转移到死信队列,从而不会造成消息丢失。

3.4.2 手动ack

开启方式简单,只需要放开此配置即可

acknowledge-mode: manual # 设置消费端手动 ack

消费消息有三种回执方法,我们来分析一下每种方法的含义。

1、basicAck
basicAck:表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。

void basicAck(long deliveryTag, boolean multiple)

deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。手动消息确认模式下,我们可以对指定deliveryTag的消息进行ack、nack、reject等操作。

multiple:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。

举个栗子: 假设我先发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认。

2、basicNack
basicNack :表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。

void basicNack(long deliveryTag, boolean multiple, boolean requeue)

deliveryTag:表示消息投递序号。

multiple:是否批量确认。

requeue:值为 true 消息将重新入队列。

3、basicReject
basicReject:拒绝消息,与basicNack区别在于不能进行批量操作,其他用法很相似。

void basicReject(long deliveryTag, boolean requeue)

deliveryTag:表示消息投递序号。

requeue:值为 true 消息将重新入队列。

测试

package com.service.direct;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;

import java.io.IOException;

// bindings其实就是用来确定队列和交换机绑定关系
@RabbitListener(queues = {"email.direct.queue"})
@Component
public class DirectEmailService {
    @RabbitHandler
    public void messagerevice(String msg, Channel channel, Message message) throws IOException {
        try {
            // 此处省略发邮件的逻辑
            System.out.println("email-------------->" + message);
            int i = 10/0;

            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }catch (Exception e){

            //注意:参数三若设置为true,会出现死循环
            channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);


			/*
            if (message.getMessageProperties().getRedelivered()) {//判断是否已经重试过
                
                log.error("消息已重复处理失败,拒绝再次接收...");
                
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
            } else {
                
                log.error("消息即将再次返回队列处理...");
                
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); 
            }
			*/
 
        }
    }
}

注意:
1、basicNack方法的参数三设置为true会造成死循环,true是允许重试。
2、使用了手动ack后,重试次数将不起作用
3、basicNack方法执行后,消息会被移除,若存在死信队列则转移到死信队列,反之则造成消息丢失。