【尚硅谷 RabbitMQ】7、发布确认高级(SpringBoot版本):回调接口、回退消息、备份交换机

在生产环境中由于一些不明原因,导致 RabbitMQ 重启。在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。

如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?

1、发布确认SpringBoot版本

1)确认机制方案

在这里插入图片描述

2)代码架构图

在这里插入图片描述

3)配置文件

spring:
  rabbitmq:
    host: 192.168.19.101
    port: 5672
    username: admin
    password: admin
    publisher-confirm-type: correlated

在这里插入图片描述

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

4)配置类

package com.tuwer.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 土味儿
 * Date 2022/3/28
 * @version 1.0
 * 高级确认发布配置类
 */
@Configuration
public class ConfirmConfig {
    /**
     * 交换机、队列及RoutingKey
     */
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    public static final String CONFIRM_ROUTING_KEY = "key1";

    /**
     * 获取确认交换机
     * @return
     */
    @Bean("confirmExchange")
    public DirectExchange getConfirmExchange(){
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }

    /**
     * 获取确认队列
     * @return
     */
    @Bean("confirmQueue")
    public Queue getConfirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    /**
     * 绑定
     * @param queue
     * @param exchange
     * @return
     */
    @Bean
    public Binding confirmQueueBindConfirmExchange(
            @Qualifier("confirmQueue") Queue queue,
            @Qualifier("confirmExchange") DirectExchange exchange
    ){
        return BindingBuilder.bind(queue).to(exchange).with(CONFIRM_ROUTING_KEY);
    }
}

5)生产者

package com.tuwer.controller;

import com.tuwer.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 土味儿
 * Date 2022/3/28
 * @version 1.0
 */
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ConfirmProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMsg/{msg}")
    public String sendMsg(@PathVariable("msg") String msg) {
        // CorrelationData 供回退方法调用
        CorrelationData correlationData = new CorrelationData("1");
        rabbitTemplate.convertAndSend(
                ConfirmConfig.CONFIRM_EXCHANGE_NAME,
                ConfirmConfig.CONFIRM_ROUTING_KEY,
                msg,
                correlationData);

        log.info("消息:【" + msg + "】已发送!");
        return "OK!";
    }
}

6)回调接口

package com.tuwer.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @author 土味儿
 * Date 2022/3/28
 * @version 1.0
 */
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 把当前类注入到 RabbitTemplate
     * @PostConstruct 表示在执行当前类的构造时运行
     * 因为 ConfirmCallback接口是 RabbitTemplate的内部类
     */
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
    }
    /**
     * 交换机确认回调方法
     * @param correlationData 回调消息
     * @param ack 交换机是否确认收到了消息:true:收到了;false:没有收到
     * @param cause 没有收到消息的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, @Nullable String cause) {
        // 消息ID
        String id = correlationData!=null ? correlationData.getId():"";
        if(ack){
            // 收到
            log.info("交换机收到了消息!ID = {}",id);
        }else{
            // 未收到
            log.info("交换机没有没有消息!ID = {};原因:{}",id,cause);
        }
    }
}

在这里插入图片描述

7)消费者

package com.tuwer.consumer;

import com.tuwer.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author 土味儿
 * Date 2022/3/28
 * @version 1.0
 */
@Slf4j
@Component
public class ConfirmQueueConsumer {
    /**
     * 监听确认队列
     * @param message
     */
    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
    public void receiveConfirmMsg(Message message){
        String msg = new String(message.getBody());
        log.info("接收到的消息:【{}】", msg);
    }
}

8)运行测试

  • 正常发送

在这里插入图片描述

  • 向不存在的交换机发送

在这里插入图片描述

在这里插入图片描述

  • 向不存在的RoutingKey发送

在这里插入图片描述

在这里插入图片描述

2、回退消息

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何处理呢?

通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时,将消息返回给生产者

1)配置文件

spring:
  rabbitmq:
    # 省略...
    # 交换机接收确认
    publisher-confirm-type: correlated
    # 交换机回退消息
    publisher-returns: true

2)回退接口

在MyCallBack中添加回退方法

@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 把当前类注入到 RabbitTemplate
     *
     * @PostConstruct 表示在执行当前类的构造时运行
     * 因为 ConfirmCallback接口是 RabbitTemplate的内部类
     */
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 省略...
     */

    /**
     * 消息回退回调方法
     * @param returnedMessage
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.error("消息{},被交换机{}退回,退回的原因:{},路由Key:{}",
                new String(returnedMessage.getMessage().getBody())
                , returnedMessage.getExchange()
                , returnedMessage.getReplyText()
                , returnedMessage.getRoutingKey());
    }
}

在这里插入图片描述

在这里插入图片描述

3)生产者

在这里插入图片描述

4)运行测试

在这里插入图片描述

3、备份交换机

  • 有了mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置mandatory参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。

  • 如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。

  • 在RabbitMQ中,有一种备份交换机的机制存在,可以很好的应对这个问题。

    备份交换机可以理解为 RabbitMQ中交换机的“备胎”,当为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后在备份交换机下绑定一个队列,这样所有原交换机无法被路由的消息,就会都进入这个队列了。当然,还可以建立一个报警队列,用独立的消费者来进行监测和报警。

1)代码架构图

在这里插入图片描述

2)修改配置类

  • 增加 备份交换机(fanout类型)、备份队列、报警队列
  • 备份队列 绑定 备份交换机
  • 报警队列 绑定 备份交换机
  • 修改确认交换机:增加参数 withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME)
package com.tuwer.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 土味儿
 * Date 2022/3/28
 * @version 1.0
 * 高级确认发布配置类
 */
@Configuration
public class ConfirmConfig {
    /**
     * 交换机、队列及RoutingKey
     */
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";

    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    public static final String WARNING_QUEUE_NAME = "warning.queue";

    public static final String CONFIRM_ROUTING_KEY = "key1";

    /**
     * 获取确认交换机
     * @return
     */
    @Bean("confirmExchange")
    public DirectExchange getConfirmExchange(){
        //return new DirectExchange(CONFIRM_EXCHANGE_NAME);
        return ExchangeBuilder
                .directExchange(CONFIRM_EXCHANGE_NAME)
                .durable(true)
                .withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME)
                .build();
    }

    /**
     * 备份交换机
     * 类型:fanout
     * @return
     */
    @Bean("backupExchange")
    public FanoutExchange getBackupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    /**
     * 获取确认队列
     * @return
     */
    @Bean("confirmQueue")
    public Queue getConfirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    /**
     * 获取备份队列
     * @return
     */
    @Bean("backupQueue")
    public Queue getBackupQueue(){
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }

    /**
     * 获取报警队列
     * @return
     */
    @Bean("warningQueue")
    public Queue getWarningQueue(){
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }

    /**
     * 确认队列 绑定 确认交换机
     * @param queue
     * @param exchange
     * @return
     */
    @Bean
    public Binding confirmQueueBindConfirmExchange(
            @Qualifier("confirmQueue") Queue queue,
            @Qualifier("confirmExchange") DirectExchange exchange
    ){
        return BindingBuilder.bind(queue).to(exchange).with(CONFIRM_ROUTING_KEY);
    }

    /**
     * 备份队列 绑定 备份交换机
     * @param queue
     * @param exchange
     * @return
     */
    @Bean
    public Binding backupQueueBindBackupExchange(
            @Qualifier("backupQueue") Queue queue,
            @Qualifier("backupExchange") FanoutExchange exchange
    ){
        return BindingBuilder.bind(queue).to(exchange);
    }

    /**
     * 报警队列 绑定 备份交换机
     * @param queue
     * @param exchange
     * @return
     */
    @Bean
    public Binding warningQueueBindBackupExchange(
            @Qualifier("warningQueue") Queue queue,
            @Qualifier("backupExchange") FanoutExchange exchange
    ){
        return BindingBuilder.bind(queue).to(exchange);
    }
}

3)报警消费者

package com.tuwer.consumer;

import com.tuwer.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.time.LocalTime;

/**
 * @author 土味儿
 * Date 2022/3/28
 * @version 1.0
 */
@Slf4j
@Component
public class WarningConsumer {
    /**
     * 监听报警队列消息
     * @param message
     */
    @RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
    public void receiveWarningQueue(Message message){
        String msg = new String(message.getBody());
        log.info("接收到报警队列的消息:{}", msg);
    }
}

4)运行测试

在这里插入图片描述

在这里插入图片描述

  • mandatory参数与备份交换机可以一起使用
  • 如果两者同时开启,消息究竟何去何从?谁优先级高就走谁那里,经过上面结果显示: 备份交换机优先级高;当消息不能路由时,直接转到了备份交换机,而不是回退给生产者

4、实战原理图

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

土味儿~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值