SpringBoot+RabbitMQ实现消息确认机制及MQ经典面试题

一、简介

在消息队列中经常有一个问题,凭什么确定你的消息已经被消费了呢?
我们分析一下消息发送到消费的过程:

  • 消息生产者 - > rabbitmq服务器(消息发送失败)
  • rabbitmq服务器自身故障导致消息丢失
  • 消息消费者 - >rabbitmq服务(消费消息失败)
    以上都有可能导致消息消费失败!

所以我们需要实现消息确认已被消费。

rabbitmq 的消息确认分为两部分:发送消息确认 和 消息接收确认。

二、环境准备

2.1 pom依赖

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>

2.2 配置文件

application.yml文件

server:
  port: 8080
spring:
  rabbitmq:
    host: 192.168.103.10
    # 发送者开启 confirm 确认机制
    publisher-confirms: true
    # 发送者开启 return 确认机制
    publisher-returns: true
    # 设置消费端手动 ack
    listener:
      simple:
        acknowledge-mode: manual
        # 是否支持重试
        retry:
          enabled: true

application.properties文件

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

# 发送者开启 confirm 确认机制
spring.rabbitmq.publisher-confirms=true
# 发送者开启 return 确认机制
spring.rabbitmq.publisher-returns=true
####################################################
# 设置消费端手动 ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 是否支持重试
spring.rabbitmq.listener.simple.retry.enabled=true

三、RabbitMQ配置类

  1. 创建队列
  2. 创建交换机
  3. 将队列绑定到交换机上
@Configuration
public class RabbitMQConfig {
    /**
     * 定义 Exchange 和 Queue
     * 定义交换机 confirmTestExchange 和队列 confirm_test_queue ,并将队列绑定在交换机上。
     * @return
     */
    @Bean(name = "confirmTestQueue")
    public Queue confirmTestQueue() {
        return new Queue("confirm_test_queue", true, false, false);
    }

    @Bean(name = "confirmTestExchange")
    public FanoutExchange confirmTestExchange() {
        return new FanoutExchange("confirmTestExchange");
    }

    @Bean
    public Binding confirmTestFanoutExchangeAndQueue(
            @Qualifier("confirmTestExchange") FanoutExchange confirmTestExchange,
            @Qualifier("confirmTestQueue") Queue confirmTestQueue) {
        return BindingBuilder.bind(confirmTestQueue).to(confirmTestExchange);
    }
 }

四、消息发送确认与接收确认

4.1 消息发送确认

用来确认生产者 producer 将消息发送到 broker ,
broker 上的交换机 exchange 再投递给队列 queue的过程中,消息是否成功投递。
消息从 producer 到 rabbitmq broker有一个 confirmCallback 确认模式。
消息从 exchange 到 queue 投递失败有一个 returnCallback 退回模式。

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

4.1.1 ConfirmCallback确认模式

消息只要被 rabbitmq broker 接收到就会触发 confirmCallback 回调 。
实现接口 ConfirmCallback ,重写其confirm()方法,方法内有三个参数correlationData、ack、cause。
• correlationData:对象内部只有一个 id 属性,用来表示当前消息的唯一性。
• ack:消息投递到broker 的状态,true表示成功。
• cause:表示投递失败的原因。
但消息被 broker 接收到只能表示已经到达 MQ服务器,并不能保证消息一定会被投递到目标 queue 里。所以接下来需要用到 returnCallback 。

package com.lsh.rabbitmqutil;

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

/**
 * @author :LiuShihao
 * @date :Created in 2020/8/19 5:21 下午
 * @desc :消息只要被 rabbitmq broker 接收到就会触发 confirmCallback 回调 。
 * •   correlationData:对象内部只有一个 id 属性,用来表示当前消息的唯一性。
 * •   ack:消息投递到broker 的状态,true表示成功。
 * •   cause:表示投递失败的原因。
 */
@Slf4j
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {

        if (!ack) {
            log.error("消息发送异常!");
        } else {
            log.info("生产者已经收到确认,ack="+ack+", cause="+cause);//ack=true, cause=null
        }
    }
}

4.1.2 ReturnCallback 退回模式

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

package com.lsh.rabbitmqutil;

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

/**
 * @author :LiuShihao
 * @date :Created in 2020/8/19 5:28 下午
 * @desc :如果消息未能投递到目标 queue 里将触发回调 returnCallback ,
 * 一旦向 queue 投递消息未成功,这里一般会记录下当前消息的详细投递数据,方便后续做重发或者补偿等操作。
 */
@Slf4j
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("returnedMessage ===> replyCode="+replyCode+" ,replyText="+replyText+" ,exchange="+exchange+" ,routingKey="+routingKey);
    }
}

4.2 消息接收确认 (监听消息)

要比消息发送确认简单一点,因为只有一个消息回执(ack)的过程。使用@RabbitHandler注解标注的方法要增加 channel(信道)、message 两个参数。

package com.lsh.rabbitmqutil;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @author :LiuShihao
 * @date :Created in 2020/7/10 2:11 下午
 * @desc :消息接收确认
 * 消息接收确认要比消息发送确认简单一点,因为只有一个消息回执(ack)的过程。
 * 使用@RabbitHandler注解标注的方法要增加 channel(信道)、message 两个参数。
 * 消费消息有三种回执方法
 */
@Slf4j
@Component
public class ReceiverMessage1 {

    @RabbitHandler
    @RabbitListener(queues = "confirm_test_queue")
    public void processHandler(String msg, Channel channel, Message message) throws IOException {

        try {
            log.info("消费者收到消息:{}", msg);

            //TODO 具体业务
            //basicAck:表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。
            //deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。手动消息确认模式下,我们可以对指定deliveryTag的消息进行ack、nack、reject等操作。
            //multiple:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

        }  catch (Exception e) {

            if (message.getMessageProperties().getRedelivered()) {

                log.error("消息已重复处理失败,拒绝再次接收...");
                //basicReject:拒绝消息,与basicNack区别在于不能进行批量操作,其他用法很相似。
                //deliveryTag:表示消息投递序号。
                //requeue:值为 true 消息将重新入队列。
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
            } else {

                log.error("消息即将再次返回队列处理...");
                //basicNack :表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。
                //deliveryTag:表示消息投递序号。
                //multiple:是否批量确认。
                //requeue:值为 true 消息将重新入队列。
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
        }
    }
}

五、 生产消息

package com.lsh.controller;

import com.lsh.rabbitmqutil.ConfirmCallbackService;
import com.lsh.rabbitmqutil.ReturnCallbackService;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author :LiuShihao
 * @date :Created in 2020/8/20 9:14 上午
 * @desc :
 */
@RestController
public class RabbitController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @Autowired
    ConfirmCallbackService confirmCallbackService;
    @Autowired
    ReturnCallbackService returnCallbackService;

    @GetMapping("/sendrabbitmq")
    public void sendRabbitMQ(Object msg){
        /**
         * 确保消息发送失败后可以重新返回到队列中
         * 注意:yml需要配置 publisher-returns: true
         */
        rabbitTemplate.setMandatory(true);

        /**
         * 消费者确认收到消息后,手动ack回执回调处理
         */
        rabbitTemplate.setConfirmCallback(confirmCallbackService);

        /**
         * 消息投递到队列失败回调处理
         */
        rabbitTemplate.setReturnCallback(returnCallbackService);

        rabbitTemplate.convertAndSend("confirm_test_queue", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss")));
    }
}

六、测试

6.1 发送消息测试一下消息确认机制是否生效

从执行结果上看发送者发消息后成功回调,消费端成功的消费了消息
在这里插入图片描述

6.2 将消息发送不存在队列,触发退回模式

将消息发送到不存在的队列
在这里插入图片描述

6.3 将消息抛出1/0异常,消息确认失败

当消息被消费时,假如处理业务发送异常,触发消息接收确认
在这里插入图片描述

总结


  1. Producer发送消息到 Broker 会触发ConfirmCallBack确认模式
  2. 如果Broker没有投入到正确的队列里 会触发ReturnCallBack退回模式(如果投到了就不会触发)
  3. ack消息接收确认

MQ面试题

1.消息无限投递怎么解决

处理完业务逻辑后确认消息, int a = 1 / 0 发生异常后将消息重新投入队列。
但是有个问题是,业务代码一旦出现 bug 99.9%的情况是不会自动修复,一条消息会被无限投递进队列,消费端无限执行,导致了死循环。
经过测试分析发现,当消息重新投递到消息队列时,这条消息不会回到队列尾部,仍是在队列头部。

消费者会立刻消费这条消息,业务处理再抛出异常,消息再重新入队,如此反复进行。导致消息队列处理出现阻塞,导致正常消息也无法运行。

而我们当时的解决方案是,先将消息进行应答,此时消息队列会删除该条消息,同时我们再次发送该消息到消息队列,异常消息就放在了消息队列尾部,这样既保证消息不会丢失,又保证了正常业务的进行。

channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// 重新发送消息到队尾
channel.basicPublish(message.getMessageProperties().getReceivedExchange(),
                    message.getMessageProperties().getReceivedRoutingKey(), MessageProperties.PERSISTENT_TEXT_PLAIN,
                    JSON.toJSONBytes(msg));

但这种方法并没有解决根本问题,错误消息还是会时不时报错,后面优化设置了消息重试次数,达到了重试上限以后,手动确认,队列删除此消息,并将消息持久化入MySQL并推送报警,进行人工处理和定时任务做补偿。

2.消息重复消费,如何保证消息的幂等性?

如何保证 MQ 的消费是幂等性,这个需要根据具体业务而定,可以借助MySQL、或者redis 将消息持久化,通过再消息中的唯一性属性校验。

文章参考:springboot + rabbitmq 消息确认机制

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liu_Shihao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值