rebbitMQ死信队列

个人理解 死信队列 其实就是一个正常队列 只是这个队列的作用 是用来承接正常队列里面有误或者是超时的数据

交换机可以是同一个,队列名称和监听的key不一样就可以了

声明正常队列的时候 可以限制超时和对打存储消息数 如果未接入死信队列 则超时和超出数量的消息会直接丢失 接入死信队列后 数据会从队列转入到死信队列中输出

配置代码如下:

package com.mall.rabbitmq.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;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class RabbitMQConfig {

    // 普通交换机名称
    public static final String EXCHANGE_NAME = "item_topic_exchange";

    // 普通队列名称
    public static final String QUEUE_NAME = "item_queue";

    // 死信队列名称
    public static final String B_QUEUE_NAME = "xkp_queue";


    /***
     * 声明交换机
     */
    @Bean(name = "itemTopicExchange")
    public Exchange topicExchange(){
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    /***
     * 声明队列
     */
    @Bean(name = "itemQueue")
    public Queue itemQueue(){
        /**
         * 1.队列创建后 对应的元素是无法修改的
         * 2.超时的消息会分发到死信队列中
         * 3.若发出的消息超出最大存储消息数量 则会保留后面的几条在队列中 前面的消息会被移动到死信队列中
         */

        // 创建map集合:封装队列参数
        Map<String,Object> map = new HashMap<>();
        // 绑定死信交换机名称
        map.put("x-dead-letter-exchange", EXCHANGE_NAME);
        // 设置发送给死信交换机的routingKey
        map.put("x-dead-letter-routing-key", "xkp.news");
        // 设置队列过期时间毫秒 未接收就会超时
        map.put("x-message-ttl", 30000);
        // 设置队列可以存储的最大消息数量
        map.put("x-max-length", 10);
        return new Queue(QUEUE_NAME, true, false, false, map);
    }

    /***
     * 队列绑定到交换机上
     */
    @Bean
    public Binding itemQueueExchange(@Qualifier("itemQueue")Queue queue,
                                     @Qualifier("itemTopicExchange")Exchange exchange){
        //绑定时添加了key 用的是topic模式 也就是可以用通配符
        return BindingBuilder.bind(queue).to(exchange).with("item.#").noargs();
    }

    /***
     * 定义死信队列
     */
    @Bean("xkpQueue")
    public Queue createDeadQueue(){
        return QueueBuilder.durable(B_QUEUE_NAME).build();
    }

    /***
     * 死信队列绑定到交换机上
     */
    @Bean
    public Binding xkpQueueExchange(@Qualifier("xkpQueue")Queue queue,
                                     @Qualifier("itemTopicExchange")Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("xkp.#").noargs();
    }

}

 ps:队列声明了参数后无法修改 除非在rebbitMQ的管理界面将对应的队列删除后重建

目前已经是集成到springboot中 所以是直接发起请求来调用对应的数据 然后通过监听队列名称来获取对应的消息 

监听消息有两种模式

一种是类上面用注解监听队列 @RabbitListener(queues = "***") 然后在方法上面用对于注解 @RabbitHandler 来承接

一种是在方法上面用直接用注解监听队列 @RabbitListener(queues = "***") 

两种方法目前在demo中未发现那种更有优势 就目前来说只是写法不同而已

 

死信模式 需要配置一个ack消息确认机制 

#设置监听端消息ACK确认模式为手动模式,默认自动确认接收消息,无论是否出异常
spring.rabbitmq.listener.simple.acknowledge-mode=manual

这个机制如果不配置的话 就会默认自动接收消息 生产者端发送的消息除非是配置错误或者发送消息时消费者端未启动 后面这种基本上不会有 因为集成到项目中了生产者和消费者是同时启动的 但是情况可以列出来

配置错误的情况下要么消息未发送到队列 要么发过去了消费者未监听该队列 未发生到队列这个也是有回调可以获取的下面会说到这个

也就是说正常情况下 如果未配置ack消息确认机制 那么死信队列其实也就没用了 因为所有的消息都被消费者确认接收了

 代码直接上 注释写的 应该算比较清楚

生产者:

package com.mall.rabbitmq.demo3;

import cn.hutool.json.JSONUtil;
import com.mall.rabbitmq.config.CustomConfirmAndReturnCallback;
import com.mall.vo.DemoVO;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/rebbit")
public class RebbitController {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    //需要注入刚刚创建的confirm回调函数
    @Autowired
    private CustomConfirmAndReturnCallback callback;

    @RequestMapping("/test")
    public String demo(){
        System.out.println("处理请求中……");
        callback.init();
        for (int i = 0; i < 3 ; i++) {
            /**
             * 发送消息 消息发送是发送到交换机中 根据对应的key值分配到不同的队列中
             * 如果对应的key值不存在与所有声明的队列导致消息发送失败,就会在ReturnCallback进行回调输出
             * 如果交换机不存在导致消息发送失败则会在ConfirmCallback进行回调输出
             * exchange:交换机
             * routingKey:消息发送key 在配置中将队列绑定到交换机中时 声明了key的值
             * Object :发送的消息
             */
            rabbitTemplate.convertAndSend("item_topic_exchange", "item.1", "demo"+i);
        }
        return "ok";
    }

    @RequestMapping("/test1")
    public String dem1o(){
        DemoVO vo = new DemoVO();
        vo.setDestination("1");
        rabbitTemplate.convertAndSend("item_topic_exchange", "item.new", JSONUtil.toJsonStr(vo));
        rabbitTemplate.convertAndSend("item_topic_exchange", "item.1", "demo");
        return "ok";
    }

}

消费者:

package com.mall.rabbitmq.demo3;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.rabbitmq.client.Channel;
import com.mall.vo.DemoVO;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;

import java.io.IOException;
@Component
//指定需要监听的队列
//@RabbitListener(queues = "item_queue")
public class MessageListener {

    /**
     * 在类上指定后 就不需要在方法中指定监听队列了
     * RabbitHandler 和 类上面RabbitListener 搭配使用
     * channel 频道对象 他提供了ack/nack方法(签收和拒绝签收)
     * Message 消息本生的封装的对象
     * String msg 消息的本身()
     */
    //@RabbitHandler
    @RabbitListener(queues = "item_queue")
    public void msg(Message message, Channel channel, String msg){
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //路由名称
            String key =  message.getMessageProperties().getReceivedRoutingKey();
            //交换机名称
            String exchange =  message.getMessageProperties().getReceivedExchange();
            //队列名称
            String queue =  message.getMessageProperties().getConsumerQueue();
            //接收消息
            DemoVO vo = JSONUtil.toBean(msg, DemoVO.class);
            System.out.println("消息下标:"+deliveryTag+";路由:"+key+";交换机:"+exchange+";队列:"+queue+";消费者接收到的消息:"+msg+";解析后:"+ (StrUtil.isEmptyIfStr(vo)?"1":vo.getDestination()));
            //int i = 1 / 0;
            //签收消息
            // 参数1 指定的是消息的序号(快递号)
            // 参数2 指定是否需要批量的签收 如果是true,那就批量 如果是false 那就不批量
            //如果一直未签收 则会在MQ中处于待确认状态 在项目重启后进入死信队列中输出 待确认消息在MQ重启后消息丢失
            channel.basicAck(deliveryTag,false);
        } catch (Exception e) {
            try {
                System.out.println(deliveryTag);
                //如果出现异常,则拒绝消息 可以重回队列 也可以丢弃 可以根据业务场景来
                //方式一:可以批量处理y用:basicNack,传三个参数
                /**
                 * 参数3 标识是否重回队列
                 * true 是重回 如果重回队列的话,异常没有解决,就会进入死循环
                 * false 就是不重回:丢弃消息
                 * 如果存在死信队列 重回队列的消息在死循环中达到队列设置的超时时间后进入死信队列
                 * 不重回的消息会进入死信队列中
                  */
                channel.basicNack(deliveryTag,false,false);
                //方式二:不批量处理:basicReject,传两个参数,第二个参数是否批量
                //channel.basicReject(deliveryTag,false);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

    /**
     * 监听死信队列的消息 简洁配置模式
     * @param message 接收到的消息体
     * @param channel 消息中心处理
     */
    @RabbitListener(queues = "xkp_queue")
    public void myListener1(Message message, Channel channel) throws IOException{
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 转换消息
            System.out.println(deliveryTag+"死信消息内容: " + new String(message.getBody()));
            // 手工签收
            channel.basicAck(deliveryTag,true);
        } catch (Exception e) {
            // 4. 拒绝签收
            // 参数3:true:重回队列,broker会重新发送该消息给消费端,false:不重回队列,消息会被丢弃
            channel.basicNack(deliveryTag,true,false);
        }
    }

    /**
     * 监听某个队列的消息 简单模式 单纯监听 由于在配置文件中开启手工签收消息 所以这个方法里面接收到的消息无法通知消息中心销毁
     * #设置监听端消息ACK确认模式为手动模式,默认自动确认接收消息,无论是否出异常
     * spring.rabbitmq.listener.simple.acknowledge-mode=manual
     * @param message 接收到的消息
     */
//    @RabbitListener(queues = "item_queue")
//    public void myListener1(String message){
//        System.out.println("消费者接收到的消息为:" + message);
//    }

    /**
     * 测试签收消息
     **/
//    @RabbitListener(bindings = @QueueBinding(
//            exchange = @Exchange(value = "item_topic_exchange",type = "topic"),
//            value = @Queue(value = "item_queue",durable = "true"),
//            key = "item.#"
//    ))
//    public void myListener1(Message message, Channel channel) throws IOException{
//        long deliveryTag = message.getMessageProperties().getDeliveryTag();
//        try {
//            if(deliveryTag%5==0){
//                System.out.println(1/0);
//            }
//            // 转换消息
//            System.out.println(deliveryTag+"消息内容: " + new String(message.getBody()));
//            // 手工签收
//            channel.basicAck(deliveryTag,false);
//
//        } catch (Exception e) {
//            // 4. 拒绝签收
//            // 参数3:true:重回队列,broker会重新发送该消息给消费端,false:不重回队列,消息会被丢弃
//            channel.basicNack(deliveryTag,true,false);
//        }
//    }
}

在什么情况下消息会到死信队列中 上面的注释基本上都写得比较清楚 

所以 对于这种机制 其实就可以玩一些需要过一定时间才需要处理的事件 就像我看到的一个小例子 用户注册七天后发送短信

这个就可以直接将普通队列的超时时间设置为7天 然后不设置监听该队列的消费者 直接监听对应的死信消息队列 到期后 数据就会转入到死信队列中 然后直接触发消息就可以了

有一个例子 就可以联想到更多的例子了 这个就自己去玩了 

 

上面说到了一个监听发送失败的 这个是基于重写 RabbitTemplate的两个方法 ConfirmCallback和ReturnCallback

直接上代码:

package com.mall.rabbitmq.config;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @Description 自定义消息发送确认的回调
 * 实现接口:implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback
 * ConfirmCallback:只确认消息是否正确到达交换机中,不管是否到达交换机,该回调都会执行;
 * ReturnCallback:如果消息从交换机未正确到达队列中将会执行,正确到达则不执行;
 */
@Component
public class CustomConfirmAndReturnCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * PostConstruct: 用于在依赖关系注入完成之后需要执行的方法上,以执行任何初始化.
     */
    @PostConstruct
    public void init() {
        //指定 ConfirmCallback
        rabbitTemplate.setConfirmCallback(this);
        //指定 ReturnCallback
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 消息从交换机成功到达队列,则returnedMessage方法不会执行;
     * 消息从交换机未能成功到达队列,则returnedMessage方法会执行;
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("退回的消息是:"+new String(message.getBody()));
        System.out.println("退回的状态码是:"+replyCode);
        System.out.println("退回的信息是:"+replyText);
        System.out.println("退回的交换机是:"+exchange);
        System.out.println("退回的路由key是:"+routingKey);
    }

    /**
     * 如果消息没有到达交换机,则该方法中isSendSuccess = false,error为错误信息;
     * 如果消息正确到达交换机,则该方法中isSendSuccess = true;
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean isSendSuccess, String error) {
        if (!isSendSuccess) {
            System.out.println("confirm回调方法>>>消息发送到交换机失败!,原因 :"+error);
        }
    }

}

注释也写的算是比较清晰 唯一一点 消息未发送到交换机 也就是 在发送的第一步消息就没发送出去的情况 这个时候会有错误原因返回但是 没有对应的消息内容返回 看了很多文档啥的 说correlationData里卖有数据 我个人测试反正一直都是空的 也不知道是不是我什么地方配置的有问题 说起配置 开启这两个回调肯定也是要有配置的 rebbitMQ里面的配置这两个都是默认关闭的 节能嘛 

配置:application.properties

#配置confirms模式,默认关闭  true表示生产者消息发出后,MQ的broker接收到了消息,发送回执表示确认接收,不设置则可能导致消息丢失
spring.rabbitmq.publisher-confirms=true
#配置returns模式,默认关闭 true表示当消息不能到达MQ的Broker端,,则使用监听器对不可达的消息做后续处理,这种一般是路由键没配好,或MQ宕机才可能发生
spring.rabbitmq.publisher-returns=true
#当上面两个为true时,这个一定要配true,否则上面两个不起作用
spring.rabbitmq.template.mandatory=true

yml配置:

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-returns: true # 实现ReturnCallback接口,如果消息从交换器发送到对应队列失败时触发
    publisher-confirm: true
    listener:
      type: simple
      simple:
        acknowledge-mode: manual # 消息消费确认,可以手动确认
        prefetch: 1 # 消费者每次从队列获取的消息数量
        concurrency: 2 # 消费者数量
        max-concurrency: 10 # 启动消费者最大数量
        # 重试策略相关配置
        retry:
          enabled: true #开启消费者重试
          max-attempts: 3 #最大重试次数
          initial-interval: 2000 #重试间隔时间
    template:
      #在消息没有被路由到合适队列情况下会将消息返还给消息发布者
      #当mandatory标志位设置为true时,如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息,
      # 那么broker会调用basic.return方法将消息返还给生产者;当mandatory设置为false时,
      # 出现上述情况broker会直接将消息丢弃;通俗的讲,mandatory标志告诉broker代理服务器至少将消息route到一个队列中,
      # 否则就将消息return给发送者;
      #: true # 启用强制信息
      mandatory: true

 

 

这些东西配置好之后 就可以直接玩了 基本上死信队列的demo就这样了

流程大概就是 初始化交换机和死信队列 初始化普通队列的时候 将一系列限制条件和死信队列绑定到普通队列中 配置文件配置ack为 manual 这样就可以开始玩死信队列了

监听消息是否发出和是否接收到 开启 confirms和 returns 以及 template.mandatory也得开 最后这个不知道为啥 反正就是要开

开启了之后 配置到生产者端这样就能获取消息发送的回调信息了

如果要玩一下的话 上面的代码可以直接用 引用的包路径上面也都有cv一下到项目中就可以测试了

 

posted @ 2021-11-01 16:15  资深CURD小白程序猿  阅读(151)  评论(0编辑  收藏  举报