RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机

一、MQ的问题

基于上篇存在的问题

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第1张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第2张图片

1. 问题说明

MQ在分布式项目中非常重要的,

它可以实现异步、削峰、解耦,但是在项目中引入MQ也会带来一系列的问题。

今天我们要解决以下几个常见的问题:

  • 消息可靠性问题:如何确保消息被成功送达消费者,并且被消费者成功消费掉

  • 延迟消息问题:如果一个消息,需要延迟15分钟再消费,像12306超时取消订单,如何实现消息的延迟投递

  • 消息堆积问题:如果消息无法被及时消费而堆积,如何解决百万级消息堆积的问题

  • MQ的高可用问题:如何避免MQ因为单点故障而不可用的问题

2. 准备代码环境

注意:为了后续的演示效果,暂不声明交换机、队列、绑定关系

创建project

  1. 删除project里的src文件夹

  2. 添加依赖坐标


    org.springframework.boot
    spring-boot-starter-parent
    2.3.9.RELEASE
    



    8
    8



    
        org.projectlombok
        lombok
    
    
    
        org.springframework.boot
        spring-boot-starter-amqp
    
    
    
        org.springframework.boot
        spring-boot-starter-test
    
    
        com.fasterxml.jackson.core
        jackson-databind
    


创建生产者模块

依赖

不需要添加,直接继承父工程的依赖

配置

修改application.yaml,添加配置:

spring:
  rabbitmq:
    host: 192.168.200.137
    port: 5672
    virtual-host: /
    username: itcast
    password: 123321

引导类

package com.mqrebbit;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProducerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProducerApplication.class,args);
    }
}

发消息测试类

package com.mqrebbit;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class Demo01SimpleTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void test(){
        rabbitTemplate.convertAndSend("demo.exchange","demo","hello");
    }
}

创建消费者模块

依赖

不需要添加,直接继承父工程的依赖

配置

修改application.yaml,添加配置:

spring:
  rabbitmq:
    host: 192.168.200.137
    port: 5672
    virtual-host: /
    username: itcast
    password: 123321
    listener:
      simple:
        prefetch: 1

引导类

package com.mqrebbit;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class,args);
    }
}

创建Listener

package com.mqrebbit.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class DemoListener {
    @RabbitListener(queues = "demo.queue")
    public void handleDemoQueueMsg(String msg){
        log.info("从{}队列接收到消息:{}", "demo.queue", msg);
        System.out.println("模拟:处理消息中……");
        log.info("消息处理完毕");
    }
}

二、消息可靠性

1. 介绍

当我们的生产者发送一条消息后,这条消息最终会到达消费者

那么在这整个过程中任何一个环境出错,都可能会导致消息的丢失,而导致不够可靠

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第3张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第4张图片

可能出问题的环节有:

  • 生产者发送消息到Broker时 丢失:

    消息未送达Exchange

    消息到达了Exchange,但未到达Queue

  • Broker收到消息后丢失:

    MQ宕机,导致未持久化保存消息

  • 消费者从Broker接收消息丢失:

    消费者接收消息后,尚未消费就宕机

针对这些问题,RabbitMQ给出了对应的解决方案

  • 生产者发送消息丢失:使用生产者确认机制

  • Broker接收消息丢失:MQ消息持久化

  • 消费者接收消息丢失:消费者确认机制与失败重试机制  

2. 生产者确认机制

介绍

在了解生产者确认机制之前,

我们需要先明确一件事:生产者发送的消息,怎么样才算是发送成功了?

消息发送成功,有两个标准

  • 消息被成功送达Exchange

  • 消息被成功送达匹配的Queue

以上两个过程任何一步失败,都认为消息发送失败了。

生产者确认机制,可以确保生产者明确知道消息是否成功发出,如果未成功的话,是哪一步出现问题。然后开发人员就可以根据投递结果做进一步处理。

Confirm Callback机制:确定回收

说明

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第5张图片

使用发送者的ConfirmCallback机制,

用于让生产者确认消息是否送达交换机

如果消息成功送达交换机,MQ会给生产者返回一个ack(确认)。

当生产者得到ack之后,就可以确定消息成功送达交换机了

使用步骤,在生产者一方做如下操作:

  1. 修改配置文件,指定 confirm确认的处理方式,使用异步方式 correlated:相关的

  2. 发送消息时,配置CorrelationData:对比数据,用于处理确认结果     

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第6张图片

声明一个交换机:

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第7张图片

示例

1. 修改配置文件

修改生产者一方的配置文件application.yaml,增加如下配置

  • 如果配置为simple,表示使用同步方式处理确认的结果

  • 如果配置为correlated,表示使用异步方式处理确认的结果,但是发送消息时需要我们准备一个CorrelationData对象,用于接收确认结果

spring:
  rabbitmq:
    #生产者确认机制类型。simple同步方式确认;correlated异步方式确认,将使用CorrelationData接收确认结果
    publisher-confirm-type: correlated

2.发送消息

package com.mqrebbit;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
public class Demo01SimpleTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void test(){
        //创建一个CorrelationData Correlation:关联
        CorrelationData data = new CorrelationData();
        //设置消息的id
        data.setId("msg-001");
        //准备好 回调函数:Callback,用于接收 将来:Future 的确认结果
        data.getFuture().addCallback(
                //Success:成功
                /**
                 * 当消息发送没有出现异常时,这个方法会被调用
                 */
                result -> {
                    if (result.isAck()){
                        log.info("发送消息完成,且已经到达交换机。消息id={}",data.getId());
                    }else {
                        log.info("发送消息完成,但是没有到达交换机。消息id={},原因={}",data.getId(),result.getReason());

                    }                },
                //Failure:失败
                /**
                 * 当消息发送出现异常时,这个方法会被调用
                 */
                ex -> {
                    log.warn("发送消息出现异常,消息id={}",data.getId(),ex);
                }
        );
        rabbitTemplate.convertAndSend("demo.exchange", "demo", "hello",data);
    }
}

3.测试结果-未送达交换机的结果

首先,我们先要保证 demo.exchange交换机不存在,再运行单元测试方法,发送消息。可看到如下结果

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第8张图片

4.测试结果-成功送达交换机

然后,我们再创建配置类,声明一个名称为demo.exchange的交换机

package com.mqrebbit.config;

import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitBindingConfig {
    //创建一个交换机
    @Bean
    public TopicExchange demoExchange(){
        return ExchangeBuilder.topicExchange("demo.exchange").build();
    }
}

然后重新发送消息,可看到如下结果:


Return Callback机制

说明

使用生产者的Confirm Callback机制,可以确保消息成功送达交换机

但是消息是否被送达队列呢?我们同样需要进行确认。

为了解决这个问题,RabbitMQ提供了Return Callback机制:

  • 如果消息被交换机成功路由到队列,一切正常

  • 如果消息路由到队列时失败了,Return回调会把消息回退给生产者。生产者可以自行决定后续要如何处理

使用步骤,在生产者一方操作:

  1. 修改配置文件,开启return callback机制,并设置强制return back 

  2. RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第9张图片

  3. 创建配置类,预先设置Return回调函数

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第10张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第11张图片


RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第12张图片

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第13张图片

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第14张图片

 示例

1. 修改配置文件

修改生产者的配置文件application.yaml,开启return回调机制

spring:
  rabbitmq:
    publisher-returns: true #开启生产者return回调机制
    template:
      mandatory: true #开启强制回调。如果为true,消息路由失败时会调用ReturnCallback回退消息;如果为false,消息路由失败时会丢弃消息

设置Return回调

当消息未被路由到Queue时,Return回调会执行

注意:

  • 只要给RabbitTemplate对象设置一次回调函数即可,并不需要每次发送消息都设置Return回调。所以我们在配置类里给RabbitTemplate设置一次即可

  • 给单例的RabbitTemplate对象设置Return回调的方式有多种,使用哪种都行,只要能够设置成功即可

创建一个配置类,在配置类里设置Return回调函数:

package com.mqrebbit.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

/*
 * ApplicationContext是: spring容器,所有bean对象都在这里面
 * ApplicationContextAware接口:当ApplicationContext初始化完成之后,
 * 接口的setApplicationContext方法将会被自动调用
 */
@Slf4j
@Configuration
public class RabbitConfig implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            //当交换机把消息路由到队列出现问题时,这个方法会自动执行
            log.warn("把消息路由到队列失败,replyCode{},replyText{},RoutingKey={},msg={}",
                    replyCode,replyText,exchange,routingKey, message);
        });
    }
}

测试结果-未路由到队列

首先,我们要先保证交换机没有绑定队列demo.queue,再运行单元测试方法,发送消息,可看到如下结果:

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第15张图片

测试结果-成功路由到队列

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第16张图片

然后,我们再找到RabbitBindingConfig配置类

增加一个队列demo.queue并绑定给交换机demo.exchange,最终代码如下:

@Configuration
public class RabbitBindingConfig {
    //创建一个交换机
    @Bean
    public TopicExchange demoExchange(){
        return ExchangeBuilder.topicExchange("demo.exchange").build();
    }
    @Bean
    public Queue demoQueue(){
        return QueueBuilder.durable("demo.queue").build();
    }
    @Bean
    public Binding demoBinding(Queue demoQueue,TopicExchange demoExchange){
        return BindingBuilder.bind(demoQueue).to(demoExchange).with("demo");
    }
}

然后再发送消息,不报错,就说明路由成功了。可以去RabbitMQ控制台上查看消息

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第17张图片

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第18张图片


3. MQ消息持久化

 介绍

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第19张图片

通过生产者确认机制,我们可以把消息投递到队列中。但是如果这时候MQ宕机了,队列里的消息同样有可能会丢失。这是因为:

  • 交换机可能是非持久化的。MQ一重启,交换机就消失了

  • 队列可能是非持久化的。MQ一重启,队列就消失了

  • 消息可能是非持久化的(在RabbitMQ内存中)。MQ一重启,消息就丢失了

所以我们必须要保证:交换机、队列、消息都是持久化的。

但实际上,我们创建交换机、队列、消息的方式都是持久化创建的所以以下内容我们仅仅了解即可

 交换机持久化

@Bean
public TopicExchange demoTopicExchange(){
    return ExchangeBuilder
            .topicExchange("demo.exchange")
        	//设置交换机为持久化的,重启也不消息。
        	//但其实可以不设置,因为交换机默认就是持久化的
            .durable(true)
            .build();
}

 队列持久化

@Bean
public Queue demoQueue(){
    return QueueBuilder
            //使用durable("队列名称")方法  创建的就是持久化队列
            .durable("demo.queue")
            .build();
}

 消息持久化

Message message = MessageBuilder
        .withBody("hello".getBytes())
        //设置为持久化消息
        .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
        .build();
rabbitTemplate.convertAndSend("demo.exchange", "demo", message, data);




​4. 消费者确认机制

介绍

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第20张图片

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第21张图片

RabbitMQ采用的是阅后即焚模式,即只要消息被消费成功获取,MQ就会立刻删除掉这条消息。所以,我们必须保证,消息确实成功的被消费掉了。为此,RabbitMQ也提供了ack确认机制:

  • RabbitMQ将消息投递给消费者

    • 消费者成功处理消息

    • 消费者向RabbitMQ返回ack确认

  • RabbitMQ收到ack确认,删除消息

从上述过程中我们可以得到,消费者返回ack的时机是非常关键的:如果消费者仅仅是得到消息还未处理,就给RabbitMQ返回ack,然后消费者宕机了,就会导致消息丢失。

SpringAMQP允许消费者使用以下三种ack模式:

  • manual:手动ack。由开发人员在处理完业务后,手动调用API,向RabbitMQ返回ack确认

  • auto:自动ack【默认】。当消费者方法正常执行完毕后,由Spring自动给RabbitMQ返回ack确认;如果出现异常,就给RabbitMQ返回nack(未消费成功)

  • none:关闭ack。MQ假定所有消息都会被成功消费,因为RabbitMQ投递消息后会立即删除

我们一般使用默认的auto模式


none模式

修改配置文件

修改消费者一方的配置文件application.yaml,设置消费者确认模式为none。添加如下配置:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none #设置 消费者确认模式为none


auto模式

修改消费者

修改消费者Listener代码,模拟处理消息出现异常的情况

@Slf4j
@Component
public class DemoListener {

    @RabbitListener(queues = "demo.queue")
    public void handleDemoQueueMsg(String msg){
        log.info("从{}队列接收到消息:{}", "simple.queue", msg);

        //模拟:处理消息中出现了异常
        int i = 1/0;

        log.info("消息处理完毕");
    }
}

测试效果

  1. 启动消费者服务

  2. 运行生产者单元测试类,发送消息

  3. 查看消费者的运行日志控制台

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第22张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第23张图片

去RabbitMQ控制台(http://192.168.200.137:15672)查看队列里的消息,发现队列里没有消息。消息还没

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第24张图片

auto模式

修改配置文件

修改消费者一方的配置文件application.yaml,设置消费者确认模式为auto

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto #设置 消费者确认模式为auto

修改消费者

代码和刚刚‘none’模式的代码相同,并没有调整。仍然是:模拟处理消息过程中出错

@Slf4j
@Component
public class DemoListener {

    @RabbitListener(queues = "demo.queue")
    public void handleDemoQueueMsg(String msg){
        log.info("从{}队列接收到消息:{}", "simple.queue", msg);

        //模拟:处理消息中出现了异常
        int i = 1/0;

        log.info("消息处理完毕");
    }
}

测试效果

  1. 重启消费者服务

  2. 运行生产者的单元测试方法,发送消息

  3. 查看消费者的运行日志控制台,发现程序在不停的报错。这是因为

    RabbitMQ在投递消息之后,消费者收到消息后抛了异常,导致没有给RabbitMQ返回ack确认

    RabbitMQ尝试重新投递消息,消费者收到消息后又抛了异常……

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第25张图片


5. 消费者失败重试

介绍

当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力:

我们可以利用Spring本身的retry机制,在消费者出现异常后,在消费者内部进行本地重试;而不是让消息重新入队列,然后让RabbitMQ重新投递。  

消费者本地重试

只要修改消费者一方的配置文件,设置消费者本地重试,并配置重试参数

修改消费者一方的配置文件application.yaml,增加如下配置:

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true  #开始 消费者本地的失败重试
          initial-interval: 1000 #初始的失败等待时长,单位是ms,默认1000
          multiplier: 1 #与上次重试间隔时长的倍数(1表示每次重试的时间间隔相同)。默认1
          max-interval: 10000 #最大重试的间隔,默认值10000 
          max-attempts: 3 #最多重试几次。默认3
          stateless: true #是否无状态。默认true。如果涉及事务,要改成false

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第26张图片

 重启消费者服务后,发现:

  1. 消费者重复获取了3次消息,在3次尝试中并没有抛出异常

  2. 在3次尝试都失败后,才抛出了RejectAndDontRequeueRecoverer异常

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第27张图片

然后再去RabbitMQ控制台(http://192.168.200.137:15672),从队列里查看消息,发现消息已经被删除了

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第28张图片
失败后的恢复策略

在刚刚的本地重试中,在达到最大次数后,消息会被丢弃,这是Spring内部机制决定的。

但是,其实在重试多次消费仍然失败后,SpringAMQP提供了MessageRecoverer接口,定义了不同的恢复策略可以用来进一步处理消息:

  • RejectAndDontRequeueRecoverer:重试次数耗尽后,直接reject,丢弃消息。是默认的处理策略

  • ImmediateRequeueMessageRecoverer:重试次数耗尽后,立即重新入队requeue

  • RepublishMessageRecoverer:重试次数耗尽后,将失败消息投递到指定的交换机

实际开发中,比较优雅的一个方案是RepublishMessageRecoverer,将失败消息重新投递到一个专门用于存储异常消息的队列中,等待后续人工处理。

使用步骤:

  1. 声明消息的恢复策略

  2. 声明交换机、队列、绑定关系

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第29张图片

声明消息恢复策略

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//使用Topic实现
@Configuration
public class RepublishMessageRecovererConfig {

    /*
     * 消息消费失败后的恢复策略:使用RepublishMessageRecoverer策略
     */
    @Bean
    public MessageRecoverer republishMsgRecoverer(RabbitTemplate rabbitTemplate) {
        return new RepublishMessageRecoverer(rabbitTemplate, "error.exchange", "error");
    }


    @Bean
    public TopicExchange errorExchange() {
        return ExchangeBuilder.topicExchange("error.exchange").build();
    }

    @Bean
    public Queue errorQueue() {
        return QueueBuilder.durable("error.queue").build();
    }

    @Bean
    public Binding errorQueueBinding(TopicExchange errorExchange, Queue errorQueue) {
        return BindingBuilder.bind(errorQueue).to(errorExchange).with("error.#");
    }
}
//使用 Direct(Routing)实现 
@Configuration
public class RepublishMessageRecovererConfig {

    /*
     * 消息消费失败后的恢复策略:使用RepublishMessageRecoverer策略
     */
    @Bean
    public MessageRecoverer republishMsgRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate, "error.exchange", "error");
    }


    @Bean
    public DirectExchange errorExchange(){
        return ExchangeBuilder.directExchange("error.exchange").build();
    }

    @Bean
    public Queue errorQueue(){
        return QueueBuilder.durable("error.queue").build();
    }
    @Bean
    public Binding errorQueueBinding(Queue errorQueue, DirectExchange errorExchange){
        return BindingBuilder.bind(errorQueue).to(errorExchange).with("error");
    }
}

测试效果

  1. 消费者收到消息,模拟报错。耗尽重试次数

  2. 打开RabbitMQ控制台(http://192.168.200.137:15672),查看错误队列里的消息

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第30张图片RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第31张图片RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第32张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第33张图片RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第34张图片


6. 小结

1. 确认消息是否送达交换机:使用Confirm Callback机制
    修改生产者的配置文件,设置 confirm确认的类型,使用correlated
    在RabbitTemplate发送消息之前,预先准备一个CorrelationData对象,设置好消息id、

    设置Confirm回调函数
        发送消息时,把CorrelationData对象设置到convertAndSend方法里
2. 确认消息是否送达队列:使用Return Callback机制
    修改生产者的配置文件,开启return机制,设置 如果消息路由失败,就强制回退消息
    给Spring容器里唯一的RabbitTemplate设置ReturnCallback回调函数:当消息路由失败

    后,会执行的回调函数
3. 确定Broker是持久化的,包括:
    交换机:需要是持久化的
    队列:需要是持久化的
    消息:需要是持久化的
4. 消费者确认机制:确认消费成功了,才让MQ删除消息
    none:不确认。MQ只要投递消息,就直接删除消息。无论是否消费成功
    manual:手动确认。需要消费者手动调用API给MQ发送ack确认,MQ到ack之后删除消息
    auto:自动确认。如果消费者方法执行中没有异常,就自动给MQ发送ack确认,MQ删除

               消息
              如果消费者方法执行中出现异常,默认 会把消息重新入队requeue,重新投递给

              消费者,死循环……

         
5. 设置消费者本地重试
    当消费者消费失败后,不要重新入队requeue,而在消费者服务本地进行重试
    当重试次数耗尽之后,默认会丢弃消息
    使用方式:
        修改消费者的配置文件,设置retry为true
6. 如果消费者本地重试次数耗尽之后,有最终的恢复策略(回收策略)
    RejectAndDontRequeueRecoverer:向MQ发送一个reject通知,丢弃消息
    ImmediateRequeueMessageRecoverer:消息重新入队列Queue
    RepublishMessageRecoverer:把消息投递给另外一个指定的交换机,重新路由到新的队

    列里,后续可以人工干预再处理

三、死信交换机

1. 介绍

什么是死信

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false

       拒绝消息不再重新入队

  • 消息是一个过期消息,超时无人消费

  • 要投递的队列消息满了,无法投递

默认情况下,死信会直接丢弃

死信交换机    

死信交换机就是普通交换机

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第35张图片

如果这个包含死信的队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,检查DLX)。

如图,一个消息被消费者拒绝了,变成了死信;因为demo.queue绑定了死信交换机 dl.direct,因此死信会投递给这个交换机;如果这个死信交换机也绑定了一个队列,则消息最终会进入这个存放死信的队列:

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第36张图片

注意:

  • 死信交换机,其实是普通交换机。只是用于处理死信,所以称为死信交换机

  • 死信队列,其实也是普通队列。只是用于处理死信,所以称为死信队列

      死信交换机都与队列有关系

2. 消费失败成为死信【拓展】

在失败重试策略中,默认的RejectAndDontRequeueRecoverer会在本地重试次数耗尽后,发送reject给RabbitMQ,消息变成死信,被丢弃。

我们可以给demo.queue添加一个死信交换机,给死信交换机绑定一个队列。这样消息变成死信后也不会丢弃,而是最终投递到死信交换机,路由到与死信交换机绑定的队列。

取消恢复策略

消费者服务中RabbitMsgRecovererConfig配置类里的恢复策略注释掉,则SpringAMQP将会使用默认的RejectAndDontRequeueRecoverer策略,在本地重试次数耗尽后,发送reject给RabbitMQ

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第37张图片
配置死信交换机

 修改生产者的配置类,在声明队列时绑定死信交换机

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第38张图片RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第39张图片

1. 声明死信交换机RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第40张图片

 2.声明队列

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第41张图片

@Configuration
public class RabbitBindingConfig {

    @Bean//声明一个交换机
    public TopicExchange demoExchange() {
        return ExchangeBuilder.topicExchange("demo.exchange").build();
    }

    @Bean //声明一个队列
    public Queue demoQueue() {
        return QueueBuilder.durable("demo.queue")
                //给队列绑定死信交换机,名称为dl.exchange
                .deadLetterExchange("dl.exchange")
                //如果队列信息成为死信,在投递给死信交换机的时候,需要携带的RoutingKey为dl
                .deadLetterRoutingKey("dl")
                .build();
    }

    @Bean
    public Binding demoBinding(Queue demoQueue, TopicExchange demoExchange) {
        return BindingBuilder.bind(demoQueue).to(demoExchange).with("demo");
    }

    //-------------------------死信交换机、死信队列、绑定关系---------------------------------------
    @Bean
    public DirectExchange dlExchange() {
        return ExchangeBuilder.directExchange("dl.exchange").build();
    }

    @Bean
    public Queue dlQueue() {
        return QueueBuilder.durable("dl.queue").build();
    }

    @Bean
    public Binding dlBinding(DirectExchange dlExchange, Queue dlQueue) {
        return BindingBuilder.bind(dlQueue).to(dlExchange).with("dl");
    }
}

测试效果

  1. 先去RabbitMQ控制台页面(http://192.168.200.137:15672)中,demo.queue队列删除掉。因为之前声明的队列并没有绑定死信交换机,必须要删除掉,重新声明才行

  2. 运行生产者的单元测试方法,发送消息

  3. 启动消费者服务,开始从demo.queue中接收消息但出现异常;在耗尽重试次数后,因为恢复策略是默认的RejectAndDontRequeueRecoverer成为死信。消息被投递到死信交换机,然后路由到死信队列

  4. 在RabbitMQ控制台中查看死信队列dl.queue,可看到死信队列中有一条消息

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第42张图片RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第43张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第44张图片

3. 消息超时成为死信

说明

如果一条消息超时未被消费,也会成为死信。而超时有两种方式:

  • 消息所在的队列设置了超时

  • 消息本身设置了超时

我们将按照如下设计,演示超时成为死信的效果:

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第45张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第46张图片


队列TTL示例

注意:为了方便演示死信队列的效果,我们将创建一个新的project,准备新的代码环境。参考第一章节中准备的代码环境。

生产者

声明队列和交换机

  • 声明死信交换机与死信队列,并绑定

  • 声明普通交换机与普通队列,并绑定。

    • 注意,声明普通队列时要:

      设置队列的TTL

      队列设置死信交换机与死信的RoutingKey

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第47张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第48张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第49张图片

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第50张图片

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第51张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第52张图片

@Configuration
public class RabbitBindingConfig {

    @Bean
    public Queue ttlQueue(){
        return QueueBuilder.durable("ttl.queue")
                //设置队列的超时时间为10s
                .ttl(10*1000)
                //给队列设置死信交换机,名称为dl.ttl.exchange;设置投递死信时的RoutingKey为ttl
                .deadLetterExchange("dl.ttl.exchange").deadLetterRoutingKey("ttl")
                .build();
    }

    @Bean
    public DirectExchange ttlExchange(){
        return ExchangeBuilder.directExchange("ttl.exchange").build();
    }

    @Bean
    public Binding ttlBinding(Queue ttlQueue, DirectExchange ttlExchange){
        return BindingBuilder.bind(ttlQueue).to(ttlExchange).with("demo");
    }

    //--------------------死信交换机、死信队列、死信绑定关系------------------------------

    @Bean
    public DirectExchange dlTtlExchange(){
        return ExchangeBuilder.directExchange("dl.ttl.exchange").build();
    }

    @Bean
    public Queue dlTtlQueue(){
        return QueueBuilder.durable("dl.ttl.queue").build();
    }

    @Bean
    public Binding tlTtlBinding(Queue dlTtlQueue, DirectExchange dlTtlExchange){
        return BindingBuilder.bind(dlTtlQueue).to(dlTtlExchange).with("ttl");
    }
}

发送消息

注意:声明队列时已经给队列设置了TTL,所以发送消息时不需要给消息设置TTL

@Slf4j
@SpringBootTest
public class DemoMessageTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void test() {
        rabbitTemplate.convertAndSend("ttl.exchange", "demo", "demo dead letter,发送时间是:" + LocalTime.now());
    }
}

消费者

@Slf4j
@Component
public class DemoListener {

    /**
     * 监听死信队列
     */
    @RabbitListener(queues = "dl.ttl.queue")
    public void handleDemoQueueMsg(String msg){
        log.info("现在时间是:{},从{}队列接收到消息:{}", LocalTime.now(), "dl.ttl.queue", msg);
    }
}

测试效果

  1. 运行生产者的单元测试代码,发送一条消息

  2. 启动消费者服务,等待接收消息。发现消费者在10s后收到了消息

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第53张图片
消息TTL示例

 生产者

声明队列和交换机

可以直接使用刚刚的“队列TTL示例”中的配置,与之相比,仅仅是声明队列时不再设置队列的TTL。代码如下:

@Configuration
public class RabbitBindingConfig {

    @Bean
    public Queue ttlQueue(){
        return QueueBuilder.durable("ttl.queue")
                //给队列设置死信交换机,名称为dl.ttl.exchange;设置投递死信时的RoutingKey为ttl
                .deadLetterExchange("dl.ttl.exchange").deadLetterRoutingKey("ttl")
                .build();
    }

    @Bean
    public DirectExchange ttlExchange(){
        return ExchangeBuilder.directExchange("ttl.exchange").build();
    }

    @Bean
    public Binding ttlBinding(Queue ttlQueue, DirectExchange ttlExchange){
        return BindingBuilder.bind(ttlQueue).to(ttlExchange).with("demo");
    }

    //--------------------死信交换机、死信队列、死信绑定关系------------------------------

    @Bean
    public DirectExchange dlTtlExchange(){
        return ExchangeBuilder.directExchange("dl.ttl.exchange").build();
    }

    @Bean
    public Queue dlTtlQueue(){
        return QueueBuilder.durable("dl.ttl.queue").build();
    }

    @Bean
    public Binding tlTtlBinding(Queue dlTtlQueue, DirectExchange dlTtlExchange){
        return BindingBuilder.bind(dlTtlQueue).to(dlTtlExchange).with("ttl");
    }
}

发送消息

发送消息时设置消息的TTL

@Slf4j
@SpringBootTest
public class DemoMessageTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void test() {
        String msgStr = "消息TTL demo,发送时间是:" + LocalTime.now();
        
        Message message = MessageBuilder
                .withBody(msgStr.getBytes())
                //设置消息TTL为5000毫秒
                .setExpiration("5000")
                .build();

        //发送消息时:
        //  如果消息和队列都设置了TTL,则哪个TTL短,哪个生效
        //  如果消息和队列只设置了一个TTL,则直接以设置的为准
        rabbitTemplate.convertAndSend("ttl.exchange", "demo", message);
    }
}

消费者

直接使用刚刚“队列TTL示例”中的消费者代码即可。代码如下:

@Slf4j
@Component
public class DemoListener {

    /**
     * 监听死信队列
     */
    @RabbitListener(queues = "dl.ttl.queue")
    public void handleDemoQueueMsg(String msg){
        log.info("现在时间是:{},从{}队列接收到消息:{}", LocalTime.now(), "dl.ttl.queue", msg);
    }
}

测试效果

  1. 运行生产者的单元测试代码,发送消息

  2. 启动消费者服务,开始监听消息。发现消费者在5s后收到了消息



4. 延迟队列(插件 )

4.1 介绍

利用TTL结合死信交换机,我们实现了消息发出后,消费者延迟收到消息的效果。这种消息模式就称为延迟队列(Delay Queue)模式。延迟队列的使用场景非常多,例如:

  • 用户下单,如果用户在15 分钟内未支付,则自动取消订单

  • 预约工作会议,20分钟后自动通知所有参会人员

因为延迟消息的需求非常多,所以RabbitMQ官方也推出了一个延迟队列插件,原生支持延迟消息功能。插件名称是:rabbitmq_delayed_message_exchange

 官网插件列表地址:Community Plugins — RabbitMQ

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第54张图片

4.2 安装

大家可以去对应的GitHub页面下载3.8.9版本的插件,这个版本的插件对应RabbitMQ3.8.5以上版本:Release v3.8.9 · rabbitmq/rabbitmq-delayed-message-exchange · GitHub

也可以直接使用资料中提供好的插件。

安装步骤如下:

  1. 查看mq的数据卷目录(docker篇创建的数据卷)

    因为我们的MQ是使用docker安装的,而创建mq容器时挂载了名称为mp-plugins的数据卷。

    我们要先查看一下数据卷的位置,执行命令:docker volumes inspect mq-plugins

    可知数据卷的目录在/var/lib/docker/volumes/mq-plugins/_data

  2. RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第55张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第56张图片

  1. 使用finalshell或其它工具,把插件上传到虚拟机的/var/lib/docker/volumes/mq-plugins/_data目录中

 

      2.进入mq容器内部:docker exec -it mq /bin/bash

 

     3.在容器内执行命令:rabbitmq-plugins enable rabbitmq_delayed_message_exchange

 

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第57张图片

4.3 原理

DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:

  • 接收消息

  • 判断消息是否具备x-delay属性

  • 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间

  • 返回routing not found结果给消息发送者

  • x-delay时间到期后,重新投递消息到指定队列

4.4 使用示例

插件的使用步骤也非常简单:

  • 声明一个交换机,交换机的类型可以是任意类型,只需要设定delayed属性为true,然后声明队列与其绑定即可。

  • 发送消息时,指定一个消息头 x-delay,值是延迟的毫秒值

声明队列和交换机

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第58张图片

 


RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第59张图片

 

 

 

@Bean
public DirectExchange delayExchange(){
    return ExchangeBuilder.directExchange("delay.direct.exchange")
            //设置为“延迟交换机”
            .delayed()
            .build();
}
@Bean
public Queue delayQueue(){
    return QueueBuilder.durable("delay.queue").build();
}
@Bean
public Binding delayBinding(Queue delayQueue, DirectExchange delayExchange){
    return BindingBuilder.bind(delayQueue).to(delayExchange).with("delay");
}

也可以使用注解方式声明,示例代码:  

@RabbitListener(bindings = @QueueBinding(
     value = @Queue("delay.queue"),
     exchange = @Exchange(value = "delay.direct.exchange", type = ExchangeTypes.DIRECT),
     key = "delay"
))
public void handleDelayQueueMsg(String msg){
 log.info("现在时间是:{},从{}队列接收到消息:{}", LocalTime.now(), "delay.queue", msg);
}

发送消息

发送消息时,必须指定x-delay,设置延迟时间

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第60张图片

 

@Test
public void test2(){
    String msgStr = "这是一条延迟消息,发送时间是:" + LocalTime.now();
    Message message = MessageBuilder
            .withBody(msgStr.getBytes())
            .setHeader("x-delay", 10000)
            .build();
    rabbitTemplate.convertAndSend("delay.direct.exchange", "delay", message);
}

监听消息

@RabbitListener(queues="delay.queue")
public void handleDelayQueueMsg(String msg){
    log.info("现在时间是:{},从{}队列接收到消息:{}", LocalTime.now(), "delay.queue", msg);
}

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第61张图片

 测试:先启动生产者--》再把消费者重启

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第62张图片

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第63张图片

 

5. 小结

1. 一条消息如何成为死信
    消息被消费失败(重试次数耗尽,并且不再重新入队),成为死信
    消息超时成为死信(无论是队列的ttl,还是消息ttl)
    队列满了成为死信
    
    MQ默认处理死信的方式是:丢弃消息
    给队列绑定死信交换机,可以把死信重新投递到新的队列里

2. 消息超时成为死信:
    给队列设置ttl:在声明队列的时候, 
        QueueBuilder.durable("队列名称")
            .ttl(毫秒值)
            .deadLetterExchange("死信交换机名称")
            .deadLetterRoutingKey("投递到死信交换机时要携带的RoutingKey")
            .build();
    给消息设置ttl:使用MessageBuilder创建消息
        MessageBuilder.withBody(消息的字节数组).setExperation("毫秒值").build();

3. 延迟消息插件的使用
    声明队列:和之前一样
    声明交换机:ExchangeBuilder.topicExchange("xxx").delayed().build();
    绑定关系:和之前一样
    
    发送消息:构造消息对象时,需要指定一个头
        MessageBuilder.withBody("消息内容字节数组").setHeader("x-delay", 毫秒值).build();

四、惰性队列

1. 消息堆积问题

什么是消息堆积

生产者发送消息速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,

直到队列存储消息达到上限。

之后发送的消息就会成为死信可能会被丢弃这就是消息堆积问题

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第64张图片

 

如何解决消息堆积

解决消息堆积两种思路:

  • 增加更多消费者,提高消费速度。也就是我们之前说的work queue模式

  • 扩大队列容积,提高堆积上限

从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列

惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存

  • 消费者要消费消息时才会从磁盘中读取并加载到内存

  • 支持数百万条消息存储

2. 惰性队列

要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可。指定属性的方式有三种:

  • 命令行方式,把一个已存在的队列修改为惰性队列

  • 基于@Bean方式声明惰性队列

  • 基于注解方式声明惰性队列

1.命令行方式

需要进入mq容器,然后执行命令:

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues  

说明:

  • rabbitmqctl:RabbitMQ的命令行管理工具

  • set_policy:设置策略。后边跟的Lazy是策略名称

  • ^lazy-queue$:是正则表达式,用于匹配队列名称。匹配上的队列都会被修改

  • {"queue-mode":"lazy"}:设置队列为lazy

  • --apply-to queues:命令的作用目标对象,是对所有队列做以上操作

2.@Bean方式

在声明队列时,调用一下lazy()方法即可

//---------------------Lazy Queue-------------------------
@Bean
public Queue lazyQueue(){
    return QueueBuilder.durable("lazy.queue")
            //设置为惰性队列
            .lazy()
            .build();
}

@Bean
public DirectExchange lazyExchange(){
    return ExchangeBuilder.directExchange("lazy.exchange").build();
}

@Bean
public Binding lazyBinding(Queue lazyQueue, DirectExchange lazyExchange){
    return BindingBuilder.bind(lazyQueue).to(lazyExchange).with("lazy");
}

 

3.注解@RabbitListener方式

在@RabbitListener注解中声明队列时,添加x-queue-mode参数

@RabbitListener(queuesToDeclare = @Queue(
        value = "lazy.queue",
        durable = "true",
        arguments = @Argument(name = "x-queue-mode", value = "lazy")
))
public void handleLazyQueueMsg(String msg) {
    log.info("从{}队列接收到消息:{}", "lazy.queue", msg);
}

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第65张图片 

 RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第66张图片

 

 

3. 小结

1. 如果MQ里消息堆积比较严重,该怎么处理
    增加消费者,提升消费消息的能力
    使用惰性队列,提升消息堆积的能力
2. 如何声明一个惰性队列
    QueueBuilder.durable("队列名称").lazy().build();    

五、MQ集群

1. 集群分类

RabbitMQ的是基于Erlang语言编写,而Erlang又是一个面向并发的语言,天然支持集群模式。RabbitMQ的集群有两种模式:

  • 普通集群:是一种分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力与堆积能力。

  • 镜像集群:是一种主从集群,普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。

镜像集群虽然支持主从,但主从同步并不是强一致的,某些情况下可能有数据丢失的风险。因此在RabbitMQ的3.8版本以后,推出了新的功能:

  • 仲裁队列:用来代替镜像集群,底层采用Raft协议确保主从的数据一致性。

2. 普通集群

介绍

普通集群,或者叫标准集群(classic cluster),具备下列特征:

  • 会在集群的各个节点间共享 元数据,包括:交换机、队列元信息。不包含队列中的消息。

  • 当访问集群某节点时,如果队列不在该节点,该节点将会承担路由的作用,从数据所在节点中获取数据并返回

  • 队列所在节点宕机,队列中的消息就会丢失

普通集群的架构如图所示:

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第67张图片

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第68张图片

 

部署

我们的计划部署3节点的mq集群:

主机名 控制台端口 amqp通信端口
mq1 15671 ---> 15672 5671 ---> 5672
mq2 15672 ---> 15672 5672 ---> 5672
mq3 15673---> 15672 5673 ---> 5672

集群中的节点标示默认都是:rabbit@[hostname],因此以上三个节点的名称分别为:

  • rabbit@mq1

  • rabbit@mq2

  • rabbit@mq3

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第69张图片

 

获取cookie

集群模式中的每个RabbitMQ 节点使用 cookie 来确定它们是否被允许相互通信,集群每个节点必须有相同的Cookie。cookie 只是一串最多 255 个字符的字母数字字符。

我们先在之前运行中的mq容器中获取一个Cookie值,作为稍后我们要搭建的集群的Cookie。

  • 执行命令:docker exec -it mq cat /var/lib/rabbitmq/.erlang.cookie

  •  

  • 可以看到Cookie值为:TCXMOWUEDEXDZSGZHUZG

删除旧容器

接下来,停止并删除当前的mq容器,我们重新搭建集群

执行命令:docker rm -f mq

 

准备集群配置

1. 准备三个文件夹

mkdir ~/01classic
cd ~/01classic
mkdir mq1 mq2 mq3

2. 准备mq1的配置文件

  1. 进入mq1文件夹:cd ~/01classic/mq1

  2. 用vi编辑rabbitmq.conf文件:vi rabbitmq.conf

    然后按i进入编辑模式,在文件中添加下面的内容,然后保存并退出vi

loopback_users.guest = false
listeners.tcp.default = 5672
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config
cluster_formation.classic_config.nodes.1 = rabbit@mq1
cluster_formation.classic_config.nodes.2 = rabbit@mq2
cluster_formation.classic_config.nodes.3 = rabbit@mq3

         3.再创建一个文件,记录Cookie:

#把cookie值保存到.erlang.cookie文件里
echo "TCXMOWUEDEXDZSGZHUZG" > ~/01classic/mq1/.erlang.cookie

拷贝配置文件

3. 拷贝配置文件

把mq1里的配置文件和cookie文件,拷贝到mq2和mq3文件夹里

cp ~/01classic/mq1/rabbitmq.conf ~/01classic/mq2
cp ~/01classic/mq1/rabbitmq.conf ~/01classic/mq3
cp ~/01classic/mq1/.erlang.cookie ~/01classic/mq2
cp ~/01classic/mq1/.erlang.cookie ~/01classic/mq3

#修改文件的权限
chmod 600 ~/01classic/mq1/.erlang.cookie
chmod 600 ~/01classic/mq2/.erlang.cookie
chmod 600 ~/01classic/mq3/.erlang.cookie

启动集群

#1. 创建虚拟网络
docker network create mq-net
#2. 创建数据卷
docker volume create 
#3. 回到家目录
cd ~
#4. 创建mq1节点
docker run -d --net mq-net \
-v ${PWD}/01classic/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/01classic/mq1/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq1 \
--hostname mq1 \
-p 5671:5672 \
-p 15671:15672 \
rabbitmq:3.8-management
#5. 创建mq2节点
docker run -d --net mq-net \
-v ${PWD}/01classic/mq2/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/01classic/mq2/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq2 \
--hostname mq2 \
-p 5672:5672 \
-p 15672:15672 \
rabbitmq:3.8-management
#6. 创建mq3节点
docker run -d --net mq-net \
-v ${PWD}/01classic/mq3/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-v ${PWD}/01classic/mq3/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq3 \
--hostname mq3 \
-p 5673:5672 \
-p 15673:15672 \
rabbitmq:3.8-management

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第70张图片

 

测试

元数据共享测试

  1. 打开mq1的控制台 http://192.168.200.137:15671,手动添加一个队列RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第71张图片

    2.打开mq2和mq3的控制台,也能看到这个队列  RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第72张图片

    数据共享测试

  2. 在mq1节点上,手动向simple.queue发送一条消息RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第73张图片

    2.在mq2和mq3上,可以查看到这条消息。其实不是数据共享,而是mq2和mq3帮我们从mq1上查询到消息,展示给我们看了  RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第74张图片

    可用性测试

     

  3. 关闭mq1容器(刚刚发送的消息,是在mq1上发送的)

    执行命令:docker stop mq1

  4. 再登录mq2或mq3的控制台,发现simple.queue不可用了

    说明:仅仅是把simple.queue的信息拷贝到了mq2和mq3,但是队列里的数据并没有拷贝过去

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第75张图片

3. 镜像集群

介绍

在刚刚的案例中,一旦创建队列的主机宕机,队列就会不可用。不具备高可用能力。如果要解决这个问题,必须使用官方提供的镜像集群方案。

镜像集群:本质是主从模式,具备下面的特征:

  • 交换机、队列、队列中的消息会在各个mq的节点之间同步备份。

  • 创建队列的节点称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。镜像队列是一主多从结构

  • 不同队列可能在任意节点上创建

  • 所有操作都是主节点完成,然后同步给镜像节点;镜像节点仅仅作为备份

  • 主宕机后,镜像节点会替代成新的主

镜像集群的架构如图所示:RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第76张图片

 

语法

镜像集群的三种模式

镜像模式的配置有3种模式:

ha-mode ha-params 效果
exactly (准确模式) count (队列的副本数量) 集群中队列副本(主服务器和镜像服务器之和)的数量。count如果为1意味着单个副本:即队列主节点。count值为2表示2个副本:1个队列主和1个队列镜像。换句话说:count = 镜像数量 + 1。如果群集中的节点数少于count,则该队列将镜像到所有节点。如果有集群总数大于count+1,并且包含镜像的节点出现故障,则将在另一个节点上创建一个新的镜像。
all (none) 队列在群集中的所有节点之间进行镜像。队列将镜像到任何新加入的节点。镜像到所有节点将对所有群集节点施加额外的压力,包括网络I / O,磁盘I / O和磁盘空间使用情况。推荐使用exactly,设置副本数为(N / 2 +1)。
nodes node names 指定队列创建到哪些节点,如果指定的节点全部不存在,则会出现异常。如果指定的节点在集群中存在,但是暂时不可用,会创建节点到当前客户端连接到的节点。

这里我们以rabbitmqctl命令作为案例来讲解配置语法。语法示例:

exactly模式

rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'

说明:

  • rabbitmqctl set_policy:固定写法

  • ha-two:策略名称,自定义

  • "^two\.":匹配队列的正则表达式,符合命名规则的队列才生效,这里是任何以two.开头的队列名称

  • '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}': 策略内容

    • "ha-mode":"exactly":策略模式,此处是exactly模式,指定副本数量

    • "ha-params":2:策略参数,这里是2,就是副本数量为2,1主1镜像

    • "ha-sync-mode":"automatic":同步策略,默认是manual,即新加入的镜像节点不会同步旧的消息。如果设置为automatic,则新加入的镜像节点会把主节点中所有消息都同步,会带来额外的网络开销

        all模式

rabbitmqctl set_policy ha-all "^all\." '{"ha-mode":"all"}'

说明:

  • ha-all:策略名称,自定义

  • "^all\.":匹配所有以all.开头的队列名

  • '{"ha-mode":"all"}':策略内容

    • "ha-mode":"all":策略模式,此处是all模式,即所有节点都会称为镜像节点

        nodes模式

rabbitmqctl set_policy ha-nodes "^nodes\." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'

说明:

  • rabbitmqctl set_policy:固定写法

  • ha-nodes:策略名称,自定义

  • "^nodes\.":匹配队列的正则表达式,符合命名规则的队列才生效,这里是任何以nodes.开头的队列名称

  • '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}': 策略内容

    • "ha-mode":"nodes":策略模式,此处是nodes模式

    • "ha-params":["rabbit@mq1", "rabbit@mq2"]:策略参数,这里指定副本所在节点名称

创建集群

我们使用exactly模式的镜像,镜像数量设置为2.

执行以下命令:

docker exec -it mq1 rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'

测试

元数据共享测试

在mq1上创建一个队列two.queue

RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第77张图片

在mq2和mq3上可以看到队列two.queue ​​​​​​​RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第78张图片 

数据共享测试

 向two.queue发送一条消息

 

在mq1、mq2、mq3任意一个节点上,都可以从two.queue队列中看到消息

其实查询消息,都是从mq1上查询得到的数据。因为two.queue在mq1节点上,mq1是主节点RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第79张图片

可用性测试

 

  1. 关闭mq1:docker stop mq1

  2. 去mq2或mq3上查看,发现two.queue仍然健康,并且切换到了mq2节点上RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第80张图片

     

4. 仲裁队列

介绍

仲裁队列:仲裁队列是RabbitMQ3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:

  • 与镜像队列一样,都是主从模式,支持主从数据同步

  • 使用非常简单,没有复杂的配置

  • 主从同步基于Raft协议,强一致

添加仲裁队列

  1. 创建队列

    Type:选择Quorum

    Name:队列名称,随便起

    Node:选择主节点RabbitMQ(2)、MQ问题:消息可靠性、延迟消息( 延迟队列(插件 ))、消息堆积(惰性队列)、MQ的高可用。ConfirmCallback机制、ReturnCallback机制、死信交换机_第81张图片

    2.查看队列   下图中“+2”字样,表示队列有2个镜像队列

    代码声明仲裁队列

    @Bean
    public Queue quorumQueue() {
        return QueueBuilder
            .durable("quorum.queue") // 持久化
            .quorum() // 仲裁队列
            .build();
    }

     

5. RabbitTemplate连接MQ集群

只要使用addresses代替掉原来的hostport即可

spring:
  rabbitmq:
    addresses: 192.168.200.137:5671, 192.168.200.137:5672, 192.168.200.137:5673
    username: itcast
    password: 123321
    virtual-host: /

6. 小结

MQ普通集群:
    集群节点之间,只会同步元数据(队列的信息、交换机的信息等等),而不会同步消息数据
    访问集群的任何一个节点,都可以访问到所有数据:因为如果数据不在本节点上,本节点会帮我们去目标节点上拉取
    队列创建到哪个节点上,一旦节点宕机,这个队列就不可用了。队列、交换机并没有备份
    
    好处:增加了消息堆积能力,增加了并发能力,提高了可用性
    缺点:队列、交换机等等没有备份,一旦所在的节点宕机,队列、交换机就不可用了

MQ镜像集群:
    集群节点之间,只会同步元数据,而不会同步消息数据
    创建的每个队列,都可以有镜像副本在其它节点上。
        主队列与镜像队列之间进行数据同步
        如果要收发消息,只有主队列可以提供服务;镜像队列仅仅作为副本进行备份,当主队列宕机时顶上
    访问集群的任何一个节点,都可以访问到所有数据:因为如果数据不在本节点上,本节点会帮我们去目标节点上拉取
    队列创建到哪个节点上,一旦节点宕机,这个队列还可以使用,因为它的镜像队列会顶上去
    
    好处:增加了消息堆积能力,增加了并发能力,提高了可用性,解决了备份的问题,某个节点宕机,队列仍然可用
    缺点:麻烦,主从之间同步不是强一致

仲裁队列:    
    是为了代替镜像队列。比镜像队列创建更简单,能实现强一致

 

 

.

你可能感兴趣的:(rabbitmq,分布式)