消息队列RabbitMQ学习笔记

消息队列MQ

第一次做笔记,不足之处希望留言指正,后面会继续改进*

1.为什么要用MQ

1.流量消峰
2.应用解耦

​ 当一个业务有多个系统调用组成时,任何一个子系统出现故障都会导致整个业务操作异常。而将这个操作转变成基于消息队列的方式后,系统间的调用被解耦。一些需要处理的信息缓存在消息队列,这样就不会导致一个子系统出现问题而整个系统异常

消息队列RabbitMQ学习笔记_第1张图片

3.异步处理

2.MQ分类

1.ActiveMq

image-20210809225327258

2.KafKa,大数据处理常用,吞吐量高(百万级)

缺点:

image-20210809225452421

3.RocketMQ

消息队列RabbitMQ学习笔记_第2张图片

RabbitMq

消息队列RabbitMQ学习笔记_第3张图片

RabbitMq

工作原理:

消息队列RabbitMQ学习笔记_第4张图片

消息队列RabbitMQ学习笔记_第5张图片

1.Hello World

2.Work queues

2.1概念:

当生产者发送大量消息时,需要有多个工作线程接收消息才能处理大量消息。(注意:一个消息只能被处理依次,不可处理多次)

消息队列RabbitMQ学习笔记_第6张图片

2.2 轮询分发消息

消息少的时候轮流发送消息

2.3消息应答

为了保证消息发送过程不丢失,RabbitMQ引入了消息应答机制:消费者在接收到消息并处理时,告诉Rabbitmq它已经处理了,Rabbitmq可以删除消息了。

2.3.3自动应答

消息在发送成功后就认为消息已经传送成功,也就是消费者就收到消息就认为消息处理成功可以删除

消息队列RabbitMQ学习笔记_第7张图片

2.3.4消息应答的方法

消息队列RabbitMQ学习笔记_第8张图片

2.3.5消息自动重新入队

如果消费者失去连接没有发送ACK确认,消息会重新排队,重新给其他消费者处理。这样,即使某个消费者意外死亡,也可以保证不会丢失消息。

2.3.6消息手动应答

消息在手动应答中是不丢失,放回队列中重新消费

生产者

public class Producer {
    //队列名称
    public static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitmqUtils.getChannel();

        //声明队列
        AMQP.Queue.DeclareOk declareOk = channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);

        //从控制台输入信息
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
            System.out.println("生产者发送消息" + message);
        }

    }
}

消费者

public class Consumer1 {
    public static final String TASK_QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException {
        Channel channel = RabbitmqUtils.getChannel();

        System.out.println("C1等待消息处理,时间短");

        DeliverCallback deliverCallback = (consumerTag , message)->{
            //沉睡1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("接收到的小的"+ new String(message.getBody()));
            //手动应答
            /**
             * 1.消息的标记,tag
             * 2.是否批量应答
             */

            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);

        };

        //取消消息时的回调
        CancelCallback cancelCallback = consumerTag->{
            System.out.println("消息消费被中断");
        };

        channel.basicConsume(TASK_QUEUE_NAME,false,deliverCallback,cancelCallback);
    }
}

2.4 持久化
队列持久化

系统重启后队列依然存在

//创建一个队列
channel.queueDeclare(QUEUQ_NAME,
                     false,false,false,null);

第二项参数为true就是一个持久化队列,

注意在队列已近存在一个非持久化的队列时需要删除重新创建才能让其变为持久化队列。

消息持久化

在发消息的时候将消息声明为持久化消息

channel.basicPublish("",QUEUQ_NAME,null,message.getBytes());

第三项参数‘props’为PERSISTENT_TEXT_PLAIN时这是是一个持久化的消息

channel.basicPublish("",QUEUQ_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());

消息队列RabbitMQ学习笔记_第9张图片

2.5不公平分发

消息队列RabbitMQ学习笔记_第10张图片

不同消费者处理消息的能力不同,为了提高消息处理速度引入不公平分发

注意:不公平分发在消费者代码中设置

channel.basicQos(1);
2.6预取值

可以提前指定消费者得到的消息数量,

channel.basicQos(x);

x代表预取值的数量

3.发布确认

在消息持久化时如果还没有保存到磁盘上,这个时候被消费者丢失会造成消息的丢失,因此引入发布确认模式,即:消息在发布后必须保存到磁盘上由生产者确认保存成功后才可以被消费。

3.1开启发布确认模式
channel.confirmSelect();
3.2单个确认发布

发一条确认一条,也就是发布一条消息后只有确认后可以发下一条。

缺点:发布速度特别慢

 Channel channel = RabbitmqUtils.getChannel();

        //队列声明
        channel.queueDeclare("q1",true,false,false,null);

        //开启发布确认
        channel.confirmSelect();

        //开始时间
        long begin = System.currentTimeMillis();

        //批量发送消息
        for(int i =0;i<100;i++){
            String msg  = i+"";
            channel.basicPublish("","q1",null,msg.getBytes(StandardCharsets.UTF_8));
            //单个消息发送后就确认
            boolean flag = channel.waitForConfirms();
            if(flag){
                System.out.println("消息发送成功");
            }
        }

        //结束时间
        long end = System.currentTimeMillis()-begin;
        System.out.println("单独确认消息耗时"+end+"ms");
    }
3.3批量确认
        //批量发送消息
        for(int i =0;i<100;i++){
            String msg  = i+"";
            channel.basicPublish("","q1",null,msg.getBytes(StandardCharsets.UTF_8));

            //达到100条时 批量确认依次
            if(i%100 == 0){
                //确认
                channel.waitForConfirms();
            }
        }
3.4 异步确认发布

先将消息全部发送,交换机确认时,如果确认收到回调ackCallback没有收到也会回调ackCallBack,这样会异步收到之前发送失败的消息,这个时候重新发送即可。

消息队列RabbitMQ学习笔记_第11张图片

 public static void m3() throws IOException, InterruptedException {
        Channel channel = RabbitmqUtils.getChannel();

        //队列声明
        channel.queueDeclare("q1",true,false,false,null);

        //开启发布确认
        channel.confirmSelect();

        //开始时间
        long begin = System.currentTimeMillis();

        //准备消息的监听器 监听哪些消息成功了 哪些消息失败了
        //消息确认成功回调的函数
        ConfirmCallback ackCallback = (deliveryTag,multiple)->{
            System.out.println("确认的消息" + deliveryTag);
        };

        //消息确认失败回调的函数
        /**
         * 参数1.消息的标记
         * 参数2.是否未批量确认
         */

        ConfirmCallback nackCallback = (deliveryTag,multiple)->{
            System.out.println("未确认的消息" + deliveryTag);
        };
        channel.addConfirmListener(ackCallback,nackCallback);

        //异步发送消息
        for(int i =0;i<1000;i++){
            String msg  = i+"";
            channel.basicPublish("","q1",null,msg.getBytes(StandardCharsets.UTF_8));

            
        }


        //结束时间
        long end = System.currentTimeMillis()-begin;
        System.out.println("批量确认消息耗时"+end+"ms");

    }

如何处理异步未确认的消息
public static void m3() throws IOException, InterruptedException {
        Channel channel = RabbitmqUtils.getChannel();

        //队列声明
        channel.queueDeclare("q1",true,false,false,null);

        //开启发布确认
        channel.confirmSelect();

        /*
        *线程安全有序的哈希表,适用于高并发
        * 1.将序号于消息进行关联
        * 2.批量删除
        * 3.支持高并发
        * */
        ConcurrentSkipListMap<Long,String> concurrentSkipListMap = new ConcurrentSkipListMap<>();

        //开始时间
        long begin = System.currentTimeMillis();

        //准备消息的监听器 监听哪些消息成功了 哪些消息失败了
        //消息确认成功回调的函数
        ConfirmCallback ackCallback = (deliveryTag,multiple)->{

            if(multiple){
                //2.删除已近确认的消息
                ConcurrentNavigableMap<Long, String> longStringConcurrentNavigableMap = concurrentSkipListMap.headMap(deliveryTag);
                longStringConcurrentNavigableMap.clear();
            }else {
                concurrentSkipListMap.remove(deliveryTag);
            }
            System.out.println("确认的消息" + deliveryTag);

        };

        //消息确认失败回调的函数
        /**
         * 参数1.消息的标记
         * 参数2.是否未批量确认
         */

        ConfirmCallback nackCallback = (deliveryTag,multiple)->{
            //3.打印为确认的消息
            String msg = concurrentSkipListMap.get(deliveryTag);
            System.out.println("未确认的消息:: " + msg );
            System.out.println("未确认的消息" + deliveryTag);


        };
        channel.addConfirmListener(ackCallback,nackCallback);

        //异步发送消息
        for(int i =0;i<1000;i++){
            String msg  = i+"";
            channel.basicPublish("","q1",null,msg.getBytes(StandardCharsets.UTF_8));
            //1.记录发送的消息
            concurrentSkipListMap.put(channel.getNextPublishSeqNo(),msg);

        }


        //结束时间
        long end = System.currentTimeMillis()-begin;
        System.out.println("批量确认消息耗时"+end+"ms");

    }

4.交换机

4.1 发布/订阅模式

消息队列RabbitMQ学习笔记_第12张图片

4.2 交换机概念:

​ RammitMQ的消息传递模型的核心思想史:生产者的消息不会直接发送到队列,通常生产者不知道消息传递到了哪个队列中

​ 因此,生产者只能将消息发送到交换机,交换机接收来自生产者的消息并将他们推入队列。

消息队列RabbitMQ学习笔记_第13张图片

4.3 Exchans的类型

交换机总共有一下类型

​ 直接(direct,路由),主题(topic),标题(headers),扇出(fanout,发布订阅的类型)

无名Exchanges

4.4临时队列

没有持久化的队列,断开连接就会删除。

创建临时队列,随机的名字

channel.queueDeclare().getQueue();
4.5绑定(binding)

交换机可以绑定多个队列,通过RoutingKey来判断发送的消息

消息队列RabbitMQ学习笔记_第14张图片

4.6 Fanout,扇出,发布订阅

这种交换机的类型会将接收到的消息广播到所有绑定的队列中去

声明一个交换机。第一个参数(交换机名字),第二个参数(交换机姓名)

 channel.exchangeDeclare("los","fanout");

绑定队列

channel.queueBind(queue,exchanges)
4.7 direct

绑定时加入routingkey

queueBind(String queue, String exchange, String routingKey, Map arguments)
4.8 topic

相比于direct和fanout功能更为强大的类型,

在tpoic中,可以根据自己*和#来指定routingKey从而达到单发部分群发以及群发等效果

其中:*代表一个单词,,,,,,#代表多个单词

注意:routingKey的大小不能超过255字节

5.死信队列

5.1死信队列的概念

消息队列RabbitMQ学习笔记_第15张图片

  • 死信队列:DLX,dead-letter-exchange
  • 利用DLX,当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX

消息队列RabbitMQ学习笔记_第16张图片

5.2死信的来源
  • 消息TTL过期
  • 队列达到最大长度(队列满了,无法再添加到数据mq中)
  • 消息被拒绝(basic.reject或basic.nack)并且requeue = false
5.2死信案例代码
5.2.1.消息TTL过期

生产者代码

public class producer {
    //普通交换机
    public static String NORMAL_EXAHANGE = "normalExchange";

    //死信交换机
    public static String DEAD_EXAHANGE = "deadExchange";

    //普通队列
    public static String NORMAL_QUEUE = "normalQueue";

    //死信队列
    public static String DEAD_QUEUE = "deadQueue";


    public static void main(String[] args) throws Exception{

        //获取信道
        Channel channel = RabbitmqUtils.getChannel();

        //死信消息,设置TTL时间10s
        AMQP.BasicProperties properties = new AMQP.BasicProperties()
                .builder().expiration("10000").build();


        for (int i =0;i<=10;i++) {
            String msg = "info" + i;
            channel.basicPublish(NORMAL_EXAHANGE, "normal", properties, msg.getBytes(StandardCharsets.UTF_8));
        }
    }
}

普通消费者代码://接收失败就发送到死信交换机

    public static void main(String[] args) throws Exception{

        //获取信道
        Channel channel = RabbitmqUtils.getChannel();


        //声明普通交换机
        channel.exchangeDeclare(NORMAL_EXAHANGE, BuiltinExchangeType.DIRECT);

        //声明死信交换机
        channel.exchangeDeclare(DEAD_EXAHANGE,BuiltinExchangeType.DIRECT);

        //声明普通队列
        Map<String, Object> arguments = new HashMap<>();
        //设置正常队列的死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXAHANGE);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key","dead");


        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);

        //声明死信队列
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);


        //绑定
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXAHANGE,"normal");
        channel.queueBind(DEAD_QUEUE,DEAD_EXAHANGE,"dead");

        //声明 接收消息
        DeliverCallback deliverCallback = (consumerTag,message)->{
            System.out.println("Consumer1接收的消息"+new String(message.getBody()));
        };

        //取消消息是的回调
        CancelCallback cancelCallback = consumerTag->{
            System.out.println("消息消费被中断");
        };

        //
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback);

    }

死信消费者代码://消费死信队列的消息,直接通过死信队列名称消费即可


        public static void main(String[] args) throws Exception{

            //获取信道
            Channel channel = RabbitmqUtils.getChannel();


            //声明 接收消息
            DeliverCallback deliverCallback = (consumerTag, message)->{
                System.out.println("Consumer1接收的消息"+new String(message.getBody()));
            };

            //取消消息时的回调
            CancelCallback cancelCallback = consumerTag->{
                System.out.println("消息消费被中断");
            };

            //
            channel.basicConsume(DEAD_QUEUE,true,deliverCallback,cancelCallback);



        }
5.2.2队列达到最大长度

修改队列声明时的代码,添加了最大长度的参数

     //声明普通队列
        Map<String, Object> arguments = new HashMap<>();
        //设置正常队列的死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXAHANGE);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key","dead");
        
        //设置正常对列的最大长度
        arguments.put("x-max-length",6);
5.2.3消息被拒

修改消息确认返回函数 //手动应答判断消息并拒绝

//拒绝

channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
//声明 接收消息
DeliverCallback deliverCallback = (consumerTag,message)->{
    String msg1 = new String(message.getBody());
    if(msg1.equals("info5")){
        System.out.println("Consumer1接收的消息"+msg1+"被拒绝");
        channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
    }else {
        System.out.println("Consumer1接收的消息" + msg1);
        channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
    }
};

//手动应答

channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,cancelCallback);

6.延迟队列

7.发布确认高级

生产环境中遇到一些不明原因,导致rabbitmq宕机或者重启,导致生产者消息投递失败,导致消息丢失,需要手动恢复和重启,为了保证消息的安全性和可靠性,需要对发布确认做一定的处理,利用confirmCallBack接口来对消息确认的结果做处理,此时必须要将确认回调接口打开,因为发布确认模式默认是关闭的,需要手动打开:

spring.rabbitmq.publisher-confirm-type=correlated

rabbitmq服务器宕机,生产者发送消息失败

消息队列RabbitMQ学习笔记_第17张图片

7.1 示例,交换机确认

创建发布确认配置类声明交换机和队列

@Configuration
public class ConfirmConfig {
 public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
 public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
 //声明业务 Exchange
 @Bean("confirmExchange")
 public DirectExchange confirmExchange(){
 return new DirectExchange(CONFIRM_EXCHANGE_NAME);
 }
 // 声明确认队列
 @Bean("confirmQueue")
 public Queue confirmQueue(){
 return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
 }
 // 声明确认队列绑定关系
 @Bean
 public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
 @Qualifier("confirmExchange") DirectExchange exchange){
 return BindingBuilder.bind(queue).to(exchange).with("key1");
 } }

消息生产者中指定消息的id通过CorrelationData ,这个是消息回调接口中用来接收消息参数,是否确认成功,以及失败原因的接口,发送一条错误的信息,如指定routingKey的队列不存在或者rabbitmq宕机:

@GetMapping("/sendMsg/{msg}")
public void sendMsg(@PathVariable(value = "msg") String msg){
    //回调接口
    CorrelationData correlationData = new CorrelationData("1");
    rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,"key2"
                                    ,msg,correlationData);
    log.info("发送消息内容为: {}",msg);
}

此时如果没有开启回调接口,会发现消息已经丢失没有做处理,配置消息回调接口:

@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback , RabbitTemplate.ReturnsCallback {

    public MyCallBack(@Autowire RabbitTemplate rabbitTemplate) {
        rabbitTemplate.setConfirmCallback(this);
    }


    /**
     * 交换机确认回调方法
     * 1. 交换机接收到了,回调
     *     1.1  correlationData 保存回调消息的ID及相关信息
     *     1.2  ack  交换机收到消息 true
     *     1.3  cause 成功没有原因 null
     * 2. 交换机没收到
     *     2.1 correlationData 保存回调消息的ID及相关信息
     *     2.2 ack  交换机没收到消息 false
     *     2.3 cause  失败的原因
     * @param correlationData 保存回调消息的ID及相关信息
     * @param ack 交换机是否收到消息
     * @param cause 失败的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : null;
        if (ack){
            log.info("交换机已经收到ID为: {}的消息",id);
        }else {
            log.info("交换机未收到ID为: {}的消息,原因为: {}",id,cause);
        }
    }
}

之后当交换机,routingkey和队列出问题后都会返回失败的信息。

7.2回退消息
7.2.1 Mandatory参数

**在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。**那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

修改配置文件

spring:
  rabbitmq:
    host: 39.106.41.153
    port: 5672
    username: root
    password: root
    publisher-confirm-type: correlated
    publisher-returns: true

回调方法

package com.vleus.rabbitmq.springbootrabbitmq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.ReturnedMessage;
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;

/**
 * @author vleus
 * @date 2021年07月28日 23:34
 */
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init() {
        //注入
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    /*
    交换机确认回调方法:
    1、发消息,交换机接收到了,回调:
        1.1 correlationData 保存回调消息的ID及相关信息;
        1.2 交换机收到消息 true
        1.3 cause null
    2、发消息 交换机接收消息失败了 回调
     2.1 correlationData 保存回调消息的ID及相关信息;
     2.2 交换机收到消息 false
     2.3 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(ReturnedMessage returned) {
        Message message = returned.getMessage(); //消息
        String exchange = returned.getExchange(); //交换机
        String replyText = returned.getReplyText(); //
        String routingKey = returned.getRoutingKey(); //路由key
        log.error("消息{},被交换机{}退回,退回的原因为: {},路由key为: {}",
                new String(message.getBody()),exchange,replyText,routingKey);
    }
}

7.3备份交换机

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

声明:

在声明交换机时声明对应的备份交换机

    //声明交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).
                durable(true)
                .alternate(BACKUP_EXCHANGE). //转发给备份交换机
                build();
//        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }


详细:
配置类

@Configuration
public class ConfirmConfig {

    //定义交换机
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";

    //队列
    public static final String CONFIRM_QUEUE = "confirm.queue";

    //routingKey
    public static final String ROUTING_KEY = "key1";

    //备份交换机
    public static final String BACKUP_EXCHANGE = "backup_exchange";

    //备份队列
    public static final String BACKUP_QUEUE = "backup_queue";

    //报警队列
    public static final String WARNING_QUEUE = "warning_queue";

    //声明交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).
                durable(true)
                .alternate(BACKUP_EXCHANGE). //转发给备份交换机
                build();
//        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }

    //声明队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE).build();
    }

    //绑定
    @Bean
    public Binding queueBindingExchange(@Qualifier("confirmQueue")Queue confirmQueue,
                                        @Qualifier("confirmExchange")DirectExchange confirmExchange) {
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(ROUTING_KEY);
    }

    //备份交换机
    @Bean("backupExchange")
    public FanoutExchange backupExchange() {
        return new FanoutExchange(BACKUP_EXCHANGE);
    }

    //声明备份队列
    @Bean("backupQueue")
    public Queue backupQueue() {
        return QueueBuilder.durable(BACKUP_QUEUE).build();
    }

    //声明报警队列
    @Bean("warningQueue")
    public Queue warningQueue() {
        return QueueBuilder.durable(WARNING_QUEUE).build();
    }

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

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

}

​ 消费者:

@Slf4j
@Component
public class WarningConsumer {

    //接收报警消息
    @RabbitListener(queues = ConfirmConfig.WARNING_QUEUE)
    public void consumeWarningQueue(Message message) {
        String msg = new String(message.getBody());

        log.error("报警发现不可路由的消息: {}",msg);
    }
}

注意:备份交换机的优先级高于回退消息

8.优先级队列

8.1使用场景

假如在我们的系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信消息提醒,但是对于淘宝来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。

注意

​ 要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费,因为这样才有机会对消息进行排序

8.2示例

生产者

public class Producer {
	 private static final String QUEUE_NAME="hello";
	 public static void main(String[] args) throws Exception {
		 try (Channel channel = RabbitMqUtils.getChannel()) {
		 //给消息赋予一个 priority 属性
		 AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
		 for (int i = 1; i <11; i++) {
			 String message = "info"+i;
			 if(i==5){
				 channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
			 }else{
			 	channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
			 }
			 	System.out.println("发送消息完成:" + message);
			 }
		 }
	 }
}

消费者

public class Consumer {
	 private static final String QUEUE_NAME="hello";
	 public static void main(String[] args) throws Exception {
		 Channel channel = RabbitMqUtils.getChannel();
		 //设置队列的最大优先级 最大可以设置到 255 官网推荐 1-10 如果设置太高比较吃内存和 CPU
		 Map<String, Object> params = new HashMap();
		 params.put("x-max-priority", 10);
		 channel.queueDeclare(QUEUE_NAME, true, false, false, params);
		 System.out.println("消费者启动等待消费......");
		 DeliverCallback deliverCallback=(consumerTag, delivery)->{
		 String receivedMessage = new String(delivery.getBody());
		 System.out.println("接收到消息:"+receivedMessage);
		 };
		 channel.basicConsume(QUEUE_NAME,true,deliverCallback,(consumerTag)->{
		 System.out.println("消费者无法消费消息时调用,如队列被删除");
		 });
	 }
}

9.惰性队列

9.1 概念

消息保存在磁盘中的队列

9.2使用场景

消费者宕机下线或者不能消费时使用惰性队列,避免大量的消息堆积在内存当中

9.3 声明方式

声明队列的时候加一个参数即可

Map<String, Object> args = new HashMap<String, Object>();
args.put(“x-queue-mode”, “lazy”);
channel.queueDeclare(“myqueue”, false, false, false, args);

10.集群

单台MQ往往不能应对生产者大量发送消息的情况,所以引入了集群。多个MQ共同构成一个集群

10.1镜像队列

RabbitMQ默认集群模式,并不包管队列的高可用性,尽管队列信息,交换机、绑定这些可以复制到集群里的任何一个节点,然则队列内容不会复制,固然该模式解决一项目组节点压力,但队列节点宕机直接导致该队列无法应用,只能守候重启,所以要想在队列节点宕机或故障也能正常应用,就要复制队列内容到集群里的每个节点,须要创建镜像队列。

10.2 Fedration插件

Fedration插件用来在不同的RabbitMQ集群之间复制队列消息,集群可以是内网也可以是公网,而这些对应用来说是透明的,即应用不会感知到,也不需要编写相关代码。

你可能感兴趣的:(rabbitmq,java)