解决RabbitMq消息丢失(发布确认回调 and 备份交换机)

5 篇文章 0 订阅
5 篇文章 0 订阅

解决交换机异常时 投递失败 造成消息丢失

当交换机发生异常时,为了防止消息丢失,我们需要加入一种回调机制, 就是不管交换机有没有接收到消息,都应该回调一个消息给生产者。

怎么做

配置文件配置

 spring.rabbitmq.publisher-confirm-type= correlated   # 开启回调   

配置类代码

package com.leava.cloud.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Description
 * @Author mq
 * @Version V1.0.0
 * @Date 2021/6/21 0021
 * 发布确认 思考
 */
@Configuration
public class ConfirmConfig {
  /**
   * 声明交换机
   */
  public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";


  /**
   * 声明队列
   */
  public static final String CONFIRM_QUEUE_NAME = "confirm.queue";


  /**
   * 声明routingKey
   */
  public static final String CONFIRM_ROUTING_KEY = "confirm.routingkey";


  @Bean
  public DirectExchange confirmExchange() {
    return new DirectExchange(CONFIRM_EXCHANGE_NAME);
  }

  @Bean
  public Queue confirmQueue() {
    return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
  }

  @Bean
  public Binding confirmQueueBindingExchange(@Qualifier("confirmExchange") DirectExchange confirmExchange,
                                             @Qualifier("confirmQueue") Queue confirmQueue) {
    return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
  }
}

生产者代码

package com.leava.cloud.controller;

import com.leava.cloud.config.ConfirmConfig;
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.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;


/**
 * @Description
 * @Author mq
 * @Version V1.0.0
 * @Date 2021/6/21 0021
 */
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ConfirmController {

  @Autowired
  private RabbitTemplate rabbitTemplate;

  @GetMapping("/sendMessage/{message}")
  public void sendMessage(@PathVariable(value = "message") String message){
    //指定消息 id 为 1
    log.info("发送消息内容:{}",message);
    CorrelationData correlationData1 =new CorrelationData("1");
    rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData1);
  }
}

消费者代码

package com.leava.cloud.monitor;

import com.leava.cloud.config.ConfirmConfig;
import com.rabbitmq.client.Channel;
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.io.UnsupportedEncodingException;

/**
 * @Description
 * @Author mq
 * @Version V1.0.0
 * @Date 2021/6/21 0021
 */
@Slf4j
@Component
public class ConfirmListen {

  @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
  public void ConfirmMsg(Message message, Channel channel) throws UnsupportedEncodingException {
    log.info("已经接收到消息:"+new String(message.getBody(),"utf-8"));
  }
}

开发回调类

需要实现RabbitTemplate 类中的ConfirmCallback接口

package com.leava.cloud.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.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @Description
 * @Author mq
 * @Version V1.0.0
 * @Date 2021/6/21 0021
 */
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {

  @Autowired
  private  RabbitTemplate rabbitTemplate;

  @PostConstruct
  public void init(){
    rabbitTemplate.setConfirmCallback(this);
  }

  /**
   * 交换机不管是否收到消息的一个回调方法
   * CorrelationData
   * 消息相关数据
   * ack
   * 交换机是否收到消息 true 是 false 否
   * cause 原因
   */
  @Override
  public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    String id=correlationData!=null?correlationData.getId():"";
    if(ack){
      log.info("交换机已经收到 id 为:{}的消息",id);
    }else{
      log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
    }
  }
}

测试

在这里插入图片描述
可以看到回调类已经接受到回调的信息 发布成功 消费者也接收到了消息。
接着我们改下交换机的参数,让消费者不能接收到消息
在这里插入图片描述
再调用一次
在这里插入图片描述
可以看到 交换机发布失败,给了生产者一个回调信息。这样保证了消息不丢失。、

解决队列异常时 发送失败 造成消息丢失

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者

怎么做

配置文件配置

spring.rabbitmq.publisher-returns: true # 开启回退 

修改回调类代码

package com.leava.cloud.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
 * @Description
 * @Author mq
 * @Version V1.0.0
 * @Date 2021/6/21 0021
 */
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {

  @Autowired
  private  RabbitTemplate rabbitTemplate;

  @PostConstruct
  public void init(){
    rabbitTemplate.setConfirmCallback(this);

    /**
     * true:
     * 交换机无法将消息进行路由时,会将该消息返回给生产者
     * false:
     * 如果发现消息无法进行路由,则直接丢弃
     */
    rabbitTemplate.setMandatory(true);
    //设置回退消息交给谁处理
    rabbitTemplate.setReturnCallback(this);
  }

  /**
   * 交换机不管是否收到消息的一个回调方法
   * CorrelationData
   * 消息相关数据
   * ack
   * 交换机是否收到消息 true 是 false 否
   * cause 原因
   */
  @Override
  public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    String id=correlationData!=null?correlationData.getId():"";
    if(ack){
      log.info("交换机已经收到 id 为:{}的消息",id);
    }else{
      log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
    }
  }

  @Override
  public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
    log.info("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}",
        new String(message.getBody()),replyText, exchange, routingKey);
  }
}

测试

修改原来的生产者代码
在这里插入图片描述
调用请求
在这里插入图片描述
可以看到因为没有可转发的路由 所以消息被退回。这是第二种因为不可路由造成消息丢失的解决方案。

备份交换机

  有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时
  发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动
  处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,
  手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增加生产者的复杂性,需要添加处理这些被
  退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文
  章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会
  进入到队列,因此无法使用死信队列来保存消息。在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的
  应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,
  当我们为某一个交换机声  明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发  到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进  入这个队列了。同时,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

怎么做

修改配置类代码

package com.leava.cloud.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Description
 * @Author mq
 * @Version V1.0.0
 * @Date 2021/6/21 0021
 * 发布确认 思考
 */
@Configuration
public class ConfirmConfig {
  /**
   * 声明交换机
   */
  public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";


  /**
   * 声明队列
   */
  public static final String CONFIRM_QUEUE_NAME = "confirm.queue";


  /**
   * 声明routingKey
   */
  public static final String CONFIRM_ROUTING_KEY = "confirm.routingkey";

  /**
   * 声明备份交换机
   */
  public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";

  /**
   * 声明备份队列
   */
  public static final String BACKUP_QUEUE_NAME = "backup.queue";

  /**
   * 声明报警队列
   */
  public static final String WARNING_QUEUE_NAME = "warning.queue";


  @Bean
  public DirectExchange confirmExchange() {
     return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
  }

  @Bean
  public Queue confirmQueue() {
    return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
  }

  @Bean
  public Binding confirmQueueBindingExchange(@Qualifier("confirmExchange") DirectExchange confirmExchange,
                                             @Qualifier("confirmQueue") Queue confirmQueue) {
    return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
  }


  @Bean
  public FanoutExchange backupExchange(){
    return new FanoutExchange(BACKUP_EXCHANGE_NAME);
  }



  @Bean
  public Queue backupQueue(){
    return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
  }

  @Bean
  public Queue warningQueue(){
    return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
  }


  @Bean
  public Binding backupExchangeBindingBackupQueue(@Qualifier("backupExchange") FanoutExchange backupExchange,
                                                  @Qualifier("backupQueue") Queue backupQueue){
    return BindingBuilder.bind(backupQueue).to(backupExchange);
  }

  @Bean
  public Binding backupExchangeBindingWarningQueue(@Qualifier("backupExchange") FanoutExchange backupExchange,
                                                   @Qualifier("warningQueue") Queue warningQueue){
    return BindingBuilder.bind(warningQueue).to(backupExchange);
  }
}

报警监听

package com.leava.cloud.monitor;

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

/**
 * @Description
 * @Author mq
 * @Version V1.0.0
 * @Date 2021/6/24 0024
 */
@Slf4j
@Component
public class WarningConsumer {


  @RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
  public void receiveWarningMsg(Message message, Channel channel){
    String msg = new String(message.getBody());
    log.error("报警发现不可路由消息:{}", msg);
  }

}

测试

在原来的生产者接口再次调用
在这里插入图片描述
已经被报警路由监听到。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

挚爱妲己~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值