【RabbitMQ】学习笔记

目录

  • 一、MQ入门
    • 1.1 消息中间件的协议
      • 1. AMQP协议
      • 2. MQTT协议
      • 3. OpenMessage协议
      • 4. Kafka协议
    • 1.2 消息分发机制
    • 1.3 消息的高可用
      • 集群模式1:Master-Slave:主从共享数据
      • 集群模式2:Master-Slave:主从同步数据
      • 集群模式3:多主集群同步部署模式
      • 集群模式4:多主集群转发部署模式
      • 集群模式5:Master-Slave与Broker-Cluster组合方案
    • 1.4 消息的高可靠
    • 1.5 MQ的使用场景
  • 二、RabbitMQ
    • 2.1 RabbitMQ安装
    • 2.2 RabbitMQ的核心概念
    • 2.3 RibbitMQ的核心组成部分
    • 2.3 RabbitMQ的运行流程
  • 三、简单入门
    • 2.1 简单模式
    • 2.2 四种exchage
      • 1 . fanout类型
        • 1) 通过图形化界面操作
          • 创建faout的交换机
          • 添加新的队列
          • 交换机与队列之间的绑定
          • 发布消息
          • 查看结果
        • 2) 核心代码
      • 2. direct类型
        • 核心代码
      • 3. topic类型
        • 核心代码
      • 4. Headers类型
        • 核心代码
    • 2.3 Work模式
      • 1. 轮询分发
      • 2. 公平分发
  • 四、整合SpringBoot
    • 4.1 fanout类型
      • 1. 生产者
      • 2. 消费者
      • 3. 测试结果
    • 4.2 direct类型
      • 1. 生产者
      • 2. 消费者
      • 3. 测试结果
    • 4.3 topic类型
      • 1. 消费者
      • 2. 生产者
      • 3. 测试结果
  • 五、RabbitMQ高级
    • 5.1 过期时间TTL
      • 1. 队列设置过期时间
      • 2. 消息设置过期时间
    • 5.2 死信队列
      • 配置类创建交换机以及队列。
      • 模拟用户进行生成订单
      • 死信消息的处理
    • 5.3 分布式事务
      • 1. 大致思路
      • 2. 消息生产:可靠生产
      • 3. 消息消费:可靠消费
  • 六、MQ监控维护
    • 6.1 内存控制
      • 1. 命令行
      • 2. 配置文件
    • 6.2 磁盘预警
      • 1. 命令行
      • 2. 配置文件
    • 6.3 内存换页
  • 附录

前言

首先,感谢学相伴的平台以及飞哥的知识分享。

本文的笔记整理于视频:【学相伴】RabbitMQ最新完整教程IDEA版通俗易懂。
代码可在Gitee上拉取: rabbitmq-demo。

本人才疏学浅,如果本文有错误之处,欢迎指正。

最后的碎碎念:

飞哥说的还是蛮不错的,认真听下去会很有收获的。我感觉飞哥的整体结构很清晰,有自己的理解成分在里面,就感觉看完之后就把知识点大纲理顺了,只不过讲述的知识点以及代码编写没有尚硅谷的那么详细。相应的,尚硅谷的就很乱,看了很蒙蔽。
个人推荐,跟B站尚硅谷发布的RabbitMQ 课件文档 1 食用更加,可以补充知识点。

一、MQ入门

为什么消息中间件采用的是http协议?

  1. http协议是比较复杂的,包含了cookie、数据的加密解密、状态码等,但是一个消息不需要这个复杂也没这个必要,主要追求的是高效、简洁、快捷;
  2. http协议是短链接,在实际的过程中,可能会出现消息的中断,不会持久化。而消息中间件需要的就是对出故障的消息进行持久化;

1.1 消息中间件的协议

1. AMQP协议

分布式事务;

消息的持久化;

高性能、高可靠的处理优势;

2. MQTT协议

物联网的重要组成部分。

低延迟、低带宽、不支持事务

3. OpenMessage协议

RocketMQ采用的协议。国内的阿里、雅虎等公司一起创作。

支持事务,持久化

4. Kafka协议

基于TCP/IP协议,采用二进制进行传输。

结构简单,不支持事务,支持持久化

1.2 消息分发机制

ActiveMQ RabbitMQ Kafka RocketMQ
发布订阅
轮询分发
公平分发
重发
消息拉取

轮询分发、公平分发它们都是保证消息只能够读取一次。

轮询分发:每个消费者消费的消息总数量是一致的;

公平分发:能者多劳,消费者性能好,处理的请求就会比较多;必须手动应答,不支持自动应答

1.3 消息的高可用

集群模式1:Master-Slave:主从共享数据

生产者将消息发送到主节点,所有的都节点连接这个消息队列共享这块的数据区域。主节点写入,一旦主节点挂掉,从节点继续服务。

集群模式2:Master-Slave:主从同步数据

与Redis的主从同步差不多

集群模式3:多主集群同步部署模式

与2差不多,写入是可以任意节点进行写入。

集群模式4:多主集群转发部署模式

元数据共享,当查找数据的时候,就会判断消息的元数据是否存在,存在则返回,否则就去问其他的消费者。

集群模式5:Master-Slave与Broker-Cluster组合方案

集群模式的总结

  • 消息共享
  • 消息同步
  • 元数据共享

1.4 消息的高可靠

  • 消息的传输:协议保证

  • 消息的存储:持久化

1.5 MQ的使用场景

流量消峰、应用解耦、异步处理

在说这个部分的时候,跟自己的业务结合一起去阐述三个场景。

二、RabbitMQ

2.1 RabbitMQ安装

首先可以进入RabbitMQ官网上查看 RabbitMQ Erlang版本要求

Linux安装视频:https://www.bilibili.com/video/BV1dX4y1V73G?p=9

Windows安装文章:https://www.cnblogs.com/saryli/p/9729591.html

Docker安装视频:https://www.bilibili.com/video/BV1dX4y1V73G?p=10

2.2 RabbitMQ的核心概念

生产者、交换机、队列、消费者

2.3 RibbitMQ的核心组成部分

【RabbitMQ】学习笔记_第1张图片

队列可以没有交换机吗?

不可以。没有指明交换机的时候,有一个默认的AMQP default交换机绑定队列,且默认的交换机是路由模式。

2.3 RabbitMQ的运行流程

【RabbitMQ】学习笔记_第2张图片

三、简单入门

RabbitMQ的七种模式,其中前五种一定要掌握

2.1 简单模式

生产者:

public class Producer {

    public static void main(String[] args) {
        // 1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        connectionFactory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            // 2. 创建链接
            connection = connectionFactory.newConnection("生产者");
            // 3. 通过链接获取通道
            channel = connection.createChannel();
            // 4. 通过通道,创建交换机、队列、绑定关系、路由key,发送消息以及接受消息
            // 队列名,持久化,排他性,自动删除,携带额外的参数
            // 将autoDelete设置为true ,当最后一个消费者消费完后并断开连接后  队列会自动进行删除
            String queueName = "queue1";
            channel.queueDeclare(queueName, false, false, false, null);
            // 5. 准备消息内容
            String message = "hello world";
            // 6. 发送消息给队列
            channel.basicPublish("", queueName, null, message.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
            // 7. 关闭通道
            if (channel!= null && channel.isOpen()){
                try {
                    channel.close();
                }catch (Exception ex){
                    ex.printStackTrace();
                }
            }
            // 8. 关闭链接
            if (connection!= null && connection.isOpen()){
                try {
                    connection.close();
                }catch (Exception ex){
                    ex.printStackTrace();
                }
            }
        }
    }
}

消费者:

public class Consumer {

    public static void main(String[] args) {
        // 1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("localhost");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        connectionFactory.setVirtualHost("/");
        Connection connection = null;
        Channel channel = null;
        try {
            // 2. 创建链接
            connection = connectionFactory.newConnection("生产者");
            // 3. 通过链接获取通道
            channel = connection.createChannel();
            // 4.通过通道,创建交换机、队列、绑定关系、路由key,发送消息以及接受消息
            // 队列名,持久化,排他性,自动删除,携带额外的参数
            channel.basicConsume("queue1", true, new DeliverCallback() {
            			// 成功的接受信息处理
                        public void handle(String consumerTag, Delivery message) throws IOException {
                            System.out.println("收到的消息是:" + new String(message.getBody(), "utf-8"));
                        }
                        // 失败接受信息处理
                    }, new CancelCallback() {
                        public void handle(String consumerTag) throws IOException {
                            System.out.println("消息接受失败");
                        }
                    }
            );

            System.out.println("消息阻断完成");
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
            // 7. 关闭通道
            if (channel!= null && channel.isOpen()){
                try {
                    channel.close();
                }catch (Exception ex){
                    ex.printStackTrace();
                }
            }
            // 8. 关闭链接
            if (connection!= null && connection.isOpen()){
                try {
                    connection.close();
                }catch (Exception ex){
                    ex.printStackTrace();
                }
            }
        }
    }
}

/**
 * 消息阻断
 * 接收到的消息是:hello world
 */
  • 是否持久化:在服务重启之后,队列是否会消失?

持久化肯定是存入磁盘。但是非持久化也会存入内存,但是重启之后,会消失;

  • 是否自动删除?

自动删除,是队列中的最后一个消息被消费时。

如果设置了自动删除,那么就会未持久化的队列就会自动删除。

如果没有设置自动删除,那么未持久化的队列还会存在,直到重启。

2.2 四种exchage

1 . fanout类型

发布与订阅模式。

交换机为fanout类型,通过交换机,向他下面的队列都发送一样的消息。

指定路由key是没有意义的,仍然会所有的队列都会收到消息。

1) 通过图形化界面操作

创建faout的交换机

如下图所示:
【RabbitMQ】学习笔记_第3张图片

添加新的队列

queue2、queue3,如下图所示:
【RabbitMQ】学习笔记_第4张图片

交换机与队列之间的绑定

web图像化界面绑定:

在队列里面进行绑定:
【RabbitMQ】学习笔记_第5张图片
在交换机里面进行绑定:
【RabbitMQ】学习笔记_第6张图片

发布消息

因为我这里是发布订阅模式,所以没有路由key。
【RabbitMQ】学习笔记_第7张图片

查看结果

【RabbitMQ】学习笔记_第8张图片
queue2、queue3:
【RabbitMQ】学习笔记_第9张图片

2) 核心代码

消费者1:

// 声明该通道的名称以及类型,是否持久化:fanout类型 名称为faout-exchang,不持久化
String exchangeName = "faout-exchang";
String type = "fanout";
channel.exchangeDeclare(exchangeName, type,true );
// 声明队列:订阅者queue1
String queueName = "queue1";
// 队列名,持久化,排他性,自动删除,携带额外的参数
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName, exchangeName, "");
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);

消费者2:

// 订阅该通道的名称以及类型:fanout类型 名称为faout-exchang
String exchangeName = "faout-exchang";
String type = "fanout";
channel.exchangeDeclare(exchangeName, type);
// 声明队列:订阅者queue2
String queueName = "queue2";
// 队列名,持久化,排他性,自动删除,携带额外的参数
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName, exchangeName, "");
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);

生产者:

// 声明发布与订阅该通道的名称以及类型
String exchangeName = "faout-exchang";
// 声明该通道的名称以及类型:fanout
channel.exchangeDeclare(exchangeName, "fanout");
// 声明该通道的交换机的类型
channel.basicPublish("faout-exchang", "", null, "hello world".getBytes());

2. direct类型

路由模式。

空的交换机有默认交换机,direct模式。

direct类型的交换机通过队列设置不同的 Routing key ,来接受不同的消息。

交换机在发消息的时候,通过指定的不同的 Routing key ,来转发到指定的 Routing key的队列。

核心代码

消费者1:

// 声明该通道的名称以及类型
String exchangeName = "direct-exchange";
String type = "direct";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue1";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey = "error";
channel.queueBind(queueName, exchangeName, routingkey);
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);

消费者2:

// 声明该通道的名称以及类型
String exchangeName = "direct-exchange";
String type = "direct";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue2";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey1 = "warning";
String routingkey2 = "info";
channel.queueBind(queueName, exchangeName, routingkey1);
channel.queueBind(queueName, exchangeName, routingkey2);
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);

生产者:

// 声明该通道的名称以及类型
String exchangeName = "direct-exchange";
String type = "direct";
String routingkey = "warning";// "info","error"
// 声明该通道的名称以及类型
channel.exchangeDeclare(exchangeName, type);
// 声明该通道的交换机的类型
channel.basicPublish(exchangeName, routingkey, null, "hello world".getBytes());

3. topic类型

主题模式

设置交换机为topic类型,通过Routing key模式匹配来分发队列里面的消息。

* : 代表着必须有1级别

# : 代表着0个或者多个级别
【RabbitMQ】学习笔记_第10张图片
Q1:

*.orange.* 代表:前面有1级,orange,后面有1级,

Q2:

*.*.rabbit代表:前面有1级,中间有1级,rabbit

lazy.# 代表:lazy后面可以有0个或者多个级别都是匹配的

测试:

com.lazy.orange : 没人收到消息

lazy.orange:Q2 收到消息,消息是匹配于lazy.#

lazy.orange.rabbit.com:Q2收到消息,消息是匹配于lazy.#

lazy.orange.rabbit:Q1 以及 Q2 均收到消息

核心代码

消费者1

// 声明该通道的名称以及类型
String exchangeName = "headers-exchange";
String type = "headers";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue2";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey = "*.orange.*";
channel.queueBind(queueName, exchangeName, routingkey);
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);

消费者2

// 声明该通道的名称以及类型
String exchangeName = "topic-exchange";
String type = "topic";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue2";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey1 = "*.*.rabbit";
String routingkey2 = "lazy.#";
channel.queueBind(queueName, exchangeName, routingkey1);
channel.queueBind(queueName, exchangeName, routingkey2);
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);

生产者:

// 声明该通道的名称以及类型
String exchangeName = "topic-exchange";
String type = "topic";
String routingkey = "com.lazy.orange";// "lazy.orange"....
// 声明该通道的名称以及类型
channel.exchangeDeclare(exchangeName, type);
// 声明该通道的交换机的类型
channel.basicPublish(exchangeName, routingkey, null, "hello world".getBytes());

4. Headers类型

交换机为Headers模式,队列设置不同的参数。

在发送消息的时候,通过不同的参数来进行消息转达到不同的队列。

To Routing key Arguments
queue1 x:1 y:1
queue2 x:1
queue3 x:2 y:1

x=1,Q2收到了

x=1,y=1,Q1、Q2都收到了

x=2,都没有队列匹配

核心代码

消费者

// 声明该通道的名称以及类型
String exchangeName = "topic-exchange";
String type = "topic";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue1";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey = "";
channel.queueBind(queueName, exchangeName, "");
// queue1的参数组
Map<String, Object> header = new HashMap<String, Object>();
header.put("x-match", "all");  //x-match: all表所有key-value全部匹配才匹配成功 ,any表只需要匹配任意一个key-value 即匹配成功。
header.put("name", "张三");
header.put("idcard","123321");
// 消息消费
Consumer consumer = new DefaultConsumer(channel){
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        String message = new String(body, "UTF-8");
        System.out.println(message);
    }
};
channel.basicConsume(queueName, true, consumer);

生产者:

// 声明该通道的名称以及类型
String exchangeName = "topic-exchange";
String type = "topic";
// 声明该通道的名称以及类型
channel.exchangeDeclare(exchangeName, type);
// 创建参数组
Map<String, Object> header = new HashMap<String, Object>();
header.put("name", "张三");
header.put("idcard","123321"); 
header.put("phone","18888888888");
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder().headers(header);
// 发送消息
String message = "Hello headers消息!";
channel.basicPublish(exchangeName, "", properties.build(), message.getBytes("UTF-8"));

2.3 Work模式

1. 轮询分发

生产者:

public class Task01 {

    // 队列名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        /**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String message = scanner.next();
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
            System.out.println("发送消息完毕:" + message);
        }
    }
}

消费者

public class Work01 {

    // 队列名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        // 获取连接通道
        Channel channel = RabbitMqUtils.getChannel();

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

        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println("消费消息被中断");
        };
        System.out.println("消费者A等待接受消息。。。。。。。。");
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);

    }
}

启动 Work01 ,看到控制台打印出信息:
【RabbitMQ】学习笔记_第11张图片
如何用Idea来模拟,有多条消费者呢?(我的Idea版本为:Idea2020.3)

结果

两位消费者循环的消费了生产者的消息。

默认情况下,RabbitMQ 会按顺序将每条消息发送给下一个消费者。平均而言,每个消费者都会收到相同数量的消息。
【RabbitMQ】学习笔记_第12张图片

2. 公平分发

消费者必须设置为手动应答机制,且设置参数 channel.basicQos(1);

int prefetchCount = 1;
channel.basicQos(prefetchCount );

意思:如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理1个任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者。

当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的 worker 或者改变其他存储任务的策略。

四、整合SpringBoot

导入依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-amqpartifactId>
dependency>

创建生产者module:springboot-rabbitmq-producer,启动类为ProducerApplication

创建消费者module:springboot-rabbitmq-comsumer,启动类为ConsumerApplication

4.1 fanout类型

1. 生产者

FanoutRabbitMqConfig : 声明交换机、队列、以及他们的绑定。

(这个配置类也可以在消费者中复制一份)

(补:建议在消费者这边,因为消费者直接跟队列进行交互)

@Configuration
public class FanoutRabbitMqConfig {
    // 1. 声明fanout交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanout_order_exchange",true,false);
    }
    // 2. 声明队列
    @Bean
    public Queue smsQueue(){
        return new Queue("sms.fanout.queue",true) ;
    }
    @Bean
    public Queue emailQueue(){
        return new Queue("email.fanout.queue",true) ;
    }    
    @Bean
    public Queue wechatQueue(){
        return new Queue("wechat.fanout.queue",true) ;
    }
    // 3. 交换机与队列之间的绑定
    @Bean
    public Binding smsBinding(){
        return BindingBuilder.bind(emailQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding emailBinding(){
        return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding wechatBinding(){
        return BindingBuilder.bind(wechatQueue()).to(fanoutExchange());
    }
}

OrderService : 业务处理类

@Service
public class OrderService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 模拟用户进行商品的下单
     *
     * @param userId 用户id
     * @param goodsId 商品id
     * @param num 数量
     * @date 2022/4/2 14:02
     */
    public void makeOrder(String userId,String goodsId, int num){
        String orderId = UUID.randomUUID().toString();
        System.out.println("订单生成成功:" + orderId);
        // MQ实现消息的转发
        String exchangeName = "fanout_order_exchange";
        String routingKey = "";
        rabbitTemplate.convertAndSend(exchangeName,routingKey,orderId);
    }
}

2. 消费者

FanoutEmailConsumer:接受email.fanout.queue的消息

// 绑定的队列名
@RabbitListener(queues = {"email.fanout.queue"})
@Component
public class FanoutEmailConsumer {
	// 处理收到的消息
    @RabbitHandler
    public void receiveMessage(String message){
        System.out.println("email fanout 接受的订单信息为:"  + message);
    }
}

FanoutSmsConsumer:接受sms.fanout.queue的消息

@RabbitListener(queues = {"sms.fanout.queue"})
@Component
public class FanoutSmsConsumer {
    @RabbitHandler
    public void receiveMessage(String message){
        System.out.println("sms fanout 接受的订单信息为:"  + message);
    }
}

FanoutWechatConsumer:接受wechat.fanout.queue的消息,与上述的代码类似。

3. 测试结果

因为生产队列以及交换机的方法,在生产模块里面,所以先启动生产模块。

创建测试类,调用生产者的业务类,生产消息。

@Test
void testFanout(){
    orderService.makeOrder("1","1",12);
}

其中,控制台的打印如下:
【RabbitMQ】学习笔记_第13张图片
运行消费者启动类ConsumerApplication,准备接受消息。

其中控制台的打印如下:
【RabbitMQ】学习笔记_第14张图片

4.2 direct类型

1. 生产者

DirectRabbitMqConfig : 声明交换机、队列、以及他们的绑定关系和routing key

@Configuration
public class DirectRabbitMqConfig {
    // 1. 声明fanout交换机
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("direct_order_exchange",true,false);
    }
    // 2. 声明队列
    @Bean
    public Queue smsDirectQueue(){
        return new Queue("sms.direct.queue",true) ;
    }
    @Bean
    public Queue emailDirectQueue(){
        return new Queue("email.direct.queue",true) ;
    }
    @Bean
    public Queue wechatDirectQueue(){
        return new Queue("wechat.direct.queue",true) ;
    }
    // 3. 交换机与队列之间的绑定
    @Bean
    public Binding smsDirectBinding(){
        return BindingBuilder.bind(smsDirectQueue()).to(directExchange()).with("sms");
    }
    @Bean
    public Binding emailDirectBinding(){
        return BindingBuilder.bind(emailDirectQueue()).to(directExchange()).with("email");
    }
    @Bean
    public Binding wechatDirectBinding(){
        return BindingBuilder.bind(wechatDirectQueue()).to(directExchange()).with("wechat");
    }

}

OrderService 里面,创建方法 makeOrderDirect。

    /**
     * 路由模式模拟用户下单
     *
     * @param userId 用户id
     * @param goodsId 商品id
     * @param num 数量
     * @param routingKey 路由key
     * @date 2022/4/2 16:36
     */
    public void makeOrderDirect(String userId,String goodsId, int num, String routingKey ){
        String orderId = UUID.randomUUID().toString();
        String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : "+ goodsId + ", num : "  + num;
        System.out.println(message);
        // MQ实现消息的转发
        String exchangeName = "direct_order_exchange";
        rabbitTemplate.convertAndSend(exchangeName,routingKey,message);
    }

2. 消费者

DirectEmailConsumer:接受email.direct.queue的消息

@RabbitListener(queues = {"email.direct.queue"})
@Component
public class DirectEmailConsumer {

    @RabbitHandler
    public void receiveMessage(String message){
        System.out.println("email direct 接受的订单信息为:"  + message);
    }

}

DirectSmsConsumer:接受 sms.direct.queue 的消息。与上述代码类似。

DirectWechatConsumer:接受 wechat.direct.queue 的消息。与上述代码类似。

3. 测试结果

因为生产队列以及交换机的方法,在生产模块里面,所以先启动生产模块。

创建测试方法,测试生产业务类的 makeOrderDirect() 方法:

@Test
void testDirect(){
    orderService.makeOrderDirect("12","12",12,"email");
    orderService.makeOrderDirect("22","22",22,"sms");
}

其中,控制台打印的方法如下:
【RabbitMQ】学习笔记_第15张图片
运行消费者启动类ConsumerApplication,准备接受消息。
在这里插入图片描述
PS:上述代码是使用配置类的方式来实现交换机与队列之间的绑定。

除此之外,还有一种是基于注解来实现交换机与队列之间的绑定关系。

建议使用配置类的方式进行统一的管理以及维护。

4.3 topic类型

该类型的演示,就使用注解来演示:

在消费者这边,修改原来的@RabbitListener,在这个注解里面,表明当前队列的参数,绑定的交换机等信息

1. 消费者

TopicWechatConsumer

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "wechat.topic.queue",durable = "true",autoDelete = "false"),
        exchange = @Exchange(value = "topic.order.exchange", type = ExchangeTypes.TOPIC),
        key = "com.#"
))
public class TopicWechatConsumer {

    @RabbitHandler
    public void receiveMessage(String message){
        System.out.println("wechat topic 接受的订单信息为:"  + message);
    }
}

TopicEmailConsumer :修改路由key以及队列的名称

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "email.topic.queue",durable = "true",autoDelete = "false"),
        exchange = @Exchange(value = "topic.order.exchange", type = ExchangeTypes.TOPIC),
        key = "*.email.#"
))
public class TopicEmailConsumer {

    @RabbitHandler
    public void receiveMessage(String message){
        System.out.println("email topic 接受的订单信息为:"  + message);
    }

}

TopicWechatConsumer与上述绑定队列与交换机的方法相同,其中路由key为 com.#

在启动并运行消费者,查看RabbitMQ的可视化界面。如下图所示,表明已经实现创建路由器以及队列并实现了他们的绑定。
【RabbitMQ】学习笔记_第16张图片

2. 生产者

    /**
     * 主题模式模拟用户下单
     *
     * @param userId 用户id
     * @param goodsId 商品id
     * @param num 数量
     * @param routingKey 路由key
     * @date 2022/4/2 16:36
     */
    public void makeOrderTopic(String userId,String goodsId, int num, String routingKey ){
        String orderId = UUID.randomUUID().toString();
        String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : "+ goodsId + ", num : "  + num;
        System.out.println(message);
        // MQ实现消息的转发
        String exchangeName = "topic.order.exchange";
        rabbitTemplate.convertAndSend(exchangeName,routingKey,message);
    }

3. 测试结果

因为生产队列以及交换机的方法,在生产模块里面,所以先启动生产模块。

创建测试方法testTopic(),测试生产业务类的 makeOrderDirect() 方法,并Debug来启动 testTopic()

@Test
void testTopic(){
    /*
     * userId = 12,wechat、email收到
     * userId = 22,wechat、email、sms全部都会收到
     */
    orderService.makeOrderTopic("12","12",12,"com.email");
    orderService.makeOrderTopic("22","22",22,"com.email.sms");
}

其中,控制台打印的方法如下:
【RabbitMQ】学习笔记_第17张图片
此刻,消费者接受消息的情况如下所示,与预期一致。
【RabbitMQ】学习笔记_第18张图片

五、RabbitMQ高级

5.1 过期时间TTL

设置消息的预期时间,只有在这个时间段之内,消息才能够被消费者接受。过了时间之后,消息就会被自动删除。

总共有两种方式设置过期时间:队列和消息。

队列设置的话,则放入队列里面的消息有过期时间。

消息设置的话,则是单独对消息进行设置。

如果在TTL队列里面,单独对消息设置了过期时间。

那么,队列的过期时间与消息的过期时间哪一个更短,就以哪一个过期时间为准。

消息在TTL队列里面,一旦超过了设置的TTL值。那么,就被称为dead message,会被投递到死信队列,消费者将无法再收到消息。(TTL消息过期,则移除)

综上所述,我们在开发中,一般使用的就是TTL队列。

1. 队列设置过期时间

使用配置类进行交换机与队列之间的绑定,此处采用的是主题模式的交换机。

其中,配置类如下所示:

@Configuration
public class RabbitmqConfig {
    @Bean
    public DirectExchange directTtlExchange() {
        return new DirectExchange("direct_ttl_exchange", true, false);
    }

    @Bean
    public Queue directTtlQueue() {
        // 设置队列参数中的的过期时间,单位毫秒,且为整型
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl", 5000);
        return new Queue("direct_ttl_queue", true,false,false,args); 
    }

    @Bean
    public Binding directTtlBinding(){
        return BindingBuilder.bind(directTtlQueue()).to(directTtlExchange()).with("ttl");
    }
}

模拟用户在往TTL的队列进行发送消息

@Test
void testTtlQueue(){
    orderService.makeTtlDirect("12","12",12,"ttl");
}

队列业务类处理。

    /**
     * 路由模式来测试过期时间TTL:设置队列的过期时间
     *
     * @param userId     用户id
     * @param goodsId    商品id
     * @param num        数量
     * @param routingKey 路由key
     * @date 2022/4/2 16:36
     */
    public void makeTtlDirect(String userId, String goodsId, int num, String routingKey) {
        String orderId = UUID.randomUUID().toString();
        String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : " + goodsId + ", num : " + num;
        System.out.println(message);
        // MQ实现消息的转发
        String exchangeName = "direct_ttl_exchange";
        rabbitTemplate.convertAndSend(exchangeName, routingKey, message);
    }

判断TTL队列是否设置成功:

查看队列的特征:
【RabbitMQ】学习笔记_第19张图片
点击队列里面,查看设置了多少的过期时间(毫秒):
【RabbitMQ】学习笔记_第20张图片

2. 消息设置过期时间

配置类指定交换机以及队列,和他们的绑定关系。

@Configuration
public class TtlMessageRabbitmqConfig {
    @Bean
    public DirectExchange directTtlMessageExchange() {
        return new DirectExchange("direct_ttl_exchange", true, false);
    }

    @Bean
    public Queue directTtlMessageQueue() {
        return new Queue("direct_ttl_message_queue", true, false, false);
    }

    @Bean
    public Binding directTtlMessageBinding() {
        return BindingBuilder.bind(directTtlMessageQueue()).to(directTtlMessageExchange()).with("ttl_message");
    }
}

业务类,设置消息的过期时间。

    /**
     * 路由模式来测试过期时间TTL:单独设置消息的过期时间
     *
     * @param userId     用户id
     * @param goodsId    商品id
     * @param num        数量
     * @param routingKey 路由key
     * @date 2022/4/2 16:36
     */
    public void makeTtlMessageDirect(String userId, String goodsId, int num, String routingKey) {
        String orderId = UUID.randomUUID().toString();
        String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : " + goodsId + ", num : " + num + routingKey;
        System.out.println(message);
        String exchangeName = "direct_ttl_message_exchange";
        // 设置消息的过期时间
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {

            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置消息的过期时间
                message.getMessageProperties().setExpiration("5000");
                // 此处,还可以设置消息的编码等。
                return message;
            }
        };
        rabbitTemplate.convertAndSend(exchangeName, routingKey, message, messagePostProcessor);
    }

测试类来模拟用户进行业务处理。

@Test
void testTtlMessage(){
    orderService.makeTtlMessageDirect("12","12",12,"ttl_message");
}

5.2 死信队列

DLX,全称为Dead-Letter-Exchange,可以称呼为死信交换机,死信邮箱。

当消息在一个队列里面变为死信。那么,它就能够被重新发送到另一个交换机中:DLX。

绑定DLX的队列称之为死信队列。

消息变成死信的原因如下:

  • 消息被拒绝
  • 消息过期
  • 队列里面的消息达到最大长度

配置类创建交换机以及队列。

  1. 过期消息的交换机以及处理过期消息的队列
@Configuration
public class DeadRabbitmqConfig {

    @Bean
    public DirectExchange deadDirectExchange(){
        return new DirectExchange("direct_dead_exchange",true,false);
    }

    @Bean
    public Queue deadQueue(){
        return new Queue("direct_dead_queue",true,false,false);
    }

    @Bean
    public Binding deadBinding(){
        return BindingBuilder.bind(deadQueue()).to(deadDirectExchange()).with("dead_message");
    }
}
  1. 在这个队列中,如果消息待够了规定的时间,则变为了过期消息,应该交由过期消息的交换机以及处理过期消息的队列
@Configuration
public class TtlQueueRabbitmqConfig {
    @Bean
    public DirectExchange directTtlExchange() {
        return new DirectExchange("direct_ttl_exchange", true, false);
    }

    @Bean
    public Queue directTtlQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl", 5000);
        // 设置队列的存放消息的最大长度
        args.put("x-max-length", 5);
        // 消息过期后绑定的死信队列是:direct_dead_exchange
        args.put("x-dead-letter-exchange", "direct_dead_exchange");
        // 因为死信队列是direct模式。通过 routing key,配置要发送给死信队列的哪一个队列。
        // 如果是fanout模式,则不需要配置 routing key
        args.put("x-dead-letter-routing-key", "dead_message");
        return new Queue("direct_ttl_queue", true, false, false, args);
    }

    @Bean
    public Binding directTtlBinding() {
        return BindingBuilder.bind(directTtlQueue()).to(directTtlExchange()).with("ttl");
    }
}

模拟用户进行生成订单

@Test
void testTtlQueue(){
    orderService.makeTtlDirect("12","12",12,"ttl");
}

public void makeTtlDirect(String userId, String goodsId, int num, String routingKey) {
    String orderId = UUID.randomUUID().toString();
    String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : " + goodsId + ", num : " + num;
    System.out.println(message);
    // MQ实现消息的转发
    String exchangeName = "direct_ttl_exchange";
    rabbitTemplate.convertAndSend(exchangeName, routingKey, message);
}

死信消息的处理

@Component
public class DeadConsumer {
    /**
     * 监听dead_order_cancel_queue队列
     * @param orderMessage
     * @param channel
     * @param correlationData
     * @param tags
     */
    @RabbitListener(queues = {"direct_dead_queue"})
    public void messageOnDeadOrderCancelQueue(String orderMessage, Channel channel, CorrelationData correlationData,
                                              @Header(AmqpHeaders.DELIVERY_TAG) long tags) throws IOException {
        try {
            System.out.println("======== 监听direct_dead_queue队列的消息 =========");
            System.out.println("======== 死信消息:"+  orderMessage + ",当前的时间为:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));;
            // 省略数据库更新订单信息

            // 手动ack
            System.out.println("======== 死信队列已经成功处理完消息");;
            channel.basicAck(tags, false);
        } catch (IOException e) {
            // 当前死信队列处理消息出错
            System.out.println("当前死信队列处理消息出错, 把消息存入数据库");
            System.out.println("当前死信队列处理消息出错, 给运维发短信");
            System.out.println("错误信息为:" + e.getMessage());
            // 丢弃消息,但是由于当前队列并未绑定死信队列,所以直接丢弃
            channel.basicNack(tags,false,false);
        }
    }
}

查看死信队列是否设置成功:
【RabbitMQ】学习笔记_第21张图片
流程演示:

【RabbitMQ】学习笔记_第22张图片

5.3 分布式事务

1. 大致思路

这里指的是消息有三种状态0,1,2。0代表消息未发送成功,1代表消息发送成功,2代表消息发送异常无法再发送
【RabbitMQ】学习笔记_第23张图片

2. 消息生产:可靠生产

【RabbitMQ】学习笔记_第24张图片

准备工作

新建goods_order(订单)表以及 order_message(订单冗余表)。其DDL语句如下:

goods_order

-- auto-generated definition
create table goods_order
(
    order_id      varchar(20) null,
    user_id       varchar(20) null,
    order_content varchar(20) null,
    create_time   datetime    null
);

order_message

-- auto-generated definition
create table order_message
(
    order_id      varchar(20) null comment '订单id',
    order_status  varchar(20) null comment '0代表消息未发送成功,1代表消息发送成功,2代表消息发送异常无法再发送

',
    order_content varchar(20) null comment ' 订单内容',
    unique_id     varchar(20) null
);

application.yaml

server:
  port: 8000

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/order_test?serverTimezone=GMT%2b8
    username: root
    password: 123456
  rabbitmq:
    password: guest
    username: guest
    # 单机
    host: localhost
    port: 5672
    # 集群
    # addresses: 127.0.0.1:5672
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual
        retry:
          # 开启手动ack,让程序去控制MQ消息的重发、删除、转移
          enabled: true
          # 最大重试次数
          max-attempts: 10
          # 重试间隔时间
          initial-interval: 2000ms
    # 开启ack确认机制        
    publisher-confirm-type: correlated


#mybatis-plus
mybatis-plus:
  #配置Mapper映射文件
  mapper-locations: classpath:/mappers/*.xml
  # 配置Mybatis数据返回类型别名(默认别名为类名)
  type-aliases-package: com.hanliy.pojo
  configuration:
    # 自动驼峰命名
    map-underscore-to-camel-case: true
    # 打印分析日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

pom.xml中的依赖如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-amqpartifactId>
    dependency>
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
    dependency>
    <dependency>
        <groupId>com.baomidougroupId>
        <artifactId>mybatis-plus-boot-starterartifactId>
        <version>3.4.2version>
    dependency>
    <dependency>
        <groupId>cn.hutoolgroupId>
        <artifactId>hutool-allartifactId>
        <version>5.7.19version>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <optional>trueoptional>
    dependency>
dependencies>

业务代码

RabbitmqConfig :配置交换机以及队列之间的绑定

order_fanout_exchange - > order_queue — (死信队列)----> dead_order_fanout_exchange -> dead_order_queue

@Configuration
public class RabbitmqConfig {

    @Bean
    public FanoutExchange deadOrderFanoutExchange() {
        return new FanoutExchange("dead_order_fanout_exchange", true, false);
    }

    @Bean
    public Queue deadOrderQueue() {
        return new Queue("dead_order_queue", true, false, false);
    }

    @Bean
    public Binding bindDeadOrder() {
        return BindingBuilder.bind(deadOrderQueue()).to(deadOrderFanoutExchange());
    }

    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("order_fanout_exchange", true, false);
    }

    @Bean
    public Queue orderQueue() {
        Map<String, Object> args = new HashMap<>();
        // 消息过期后绑定的死信队列是:dead_order_fanout_exchange
        args.put("x-dead-letter-exchange", "dead_order_fanout_exchange");
        return new Queue("order_queue", true, false, false, args);
    }

    @Bean
    public Binding bindOrder() {
        return BindingBuilder.bind(orderQueue()).to(fanoutExchange());
    }
}

OrderController:控制层

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("mq")
    public void OrderCreateMq() throws Exception {
        Order order = new Order();
        order.setOrderId("1001");
        order.setUserId("1001");
        order.setOrderContent("测试数据");
        order.setCreateTime(new Date());
        orderService.saveOrder(order);
        System.out.println("订单创建成功");
    }
}

OrderService:

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderMessageMapper orderMessageMapper;
    @Autowired
    private OrderMqService orderMqService;

    @Transactional(rollbackFor = Exception.class)
    public void saveOrder(Order order) throws Exception {
        // 订单处理
        orderMapper.insert(order);
        // 消息冗余
        OrderMessage message = new OrderMessage();
        message.setOrderId(order.getOrderId());
        message.setOrderContent(order.getOrderContent());
        message.setOrderStatus("0");
        message.setUniqueId("1");
        orderMessageMapper.insert(message);
        // mq进行消息确认处理
        orderMqService.sendMessage(order);
    }

}

OrderMqService : MQ的信息发送以及ack回调处理

@Service
public class OrderMqService {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private OrderMessageMapper orderMessageMapper;

    @PostConstruct
    public void regCallback() {
        // 消息发送成功后,给与生产者的消息回执,来确保生产者可靠性
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("cause" + cause);
                // 如果ack为true,代表消息已经收到
                String orderId = correlationData.getId();
                // 如果消息中间件收到了消息,那么就是true
                if (!ack) {
                    // 这里可能要进行其他方式进行存储
                    System.out.println("MQ队列消息应答失败,orderId为: " + orderId);
                    return;
                }
                try {
                    OrderMessage orderMessage = new OrderMessage();
                    orderMessage.setOrderId(orderId);
                    orderMessage.setOrderStatus("1");
                    int update = orderMessageMapper.update(orderMessage, null);
                    if (update == 1) {
                        System.out.println("本地消息修改成功,消息成功投递到队列中");
                    }
                } catch (Exception e) {
                    System.out.println("本地消息修改失败,出现异常:" + e.getMessage());
                }
            }
        });
    }

    public void sendMessage(Order order) {
        rabbitTemplate.convertAndSend("order_fanout_exchange", "", JSONUtil.toJsonStr(order)
                , new CorrelationData(order.getOrderId()));
    }

}

OrderTask : 定时扫描未发送成功的信息

@EnableScheduling
public class OrderTask {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Scheduled(cron = "")
    public void sendMessage() {
        /**
         * 把状态为0的订单重新发送到MQ中
         */
        //  假设这是状态为0的订单信息
        List<Order> orders = null;
        // 重新发送MQ
        for (Order order : orders) {
            // 重新发送到MQ中
            rabbitTemplate.convertAndSend("order_fanout_exchange", "", JSONUtil.toJsonStr(order)
                    , new CorrelationData(order.getOrderId()));
        }
    }
}

结果

运行结果如下所示:
【RabbitMQ】学习笔记_第25张图片
在RabbitMQ中的可视化界面中,可以看到有一条待消费的信息:
【RabbitMQ】学习笔记_第26张图片

3. 消息消费:可靠消费

【RabbitMQ】学习笔记_第27张图片

前期准备

dispatcher : 运单中心表

-- auto-generated definition
create table dispatcher
(
    dispatch_id   varchar(20) null comment '派送者id',
    order_id      varchar(20) null comment '订单id',
    order_status  int         null comment '0待派送',
    order_content varchar(20) null comment '订单内容',
    create_time   datetime    null comment '创建时间',
    user_id       varchar(20) null comment '购买人'
);

application.yaml

server:
  port: 9000

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/dispatch_test?serverTimezone=GMT%2b8
    username: root
    password: 123456
  rabbitmq:
    password: guest
    username: guest
    # 单机
    host: localhost
    port: 5672
    # 集群
    # addresses: 127.0.0.1:5672
    virtual-host: /
    listener:
      # fanout模式的重试
      simple:
        acknowledge-mode: manual
        retry:
          # 开启手动ack,让程序去控制MQ消息的重发、删除、转移
          enabled: true
          # 最大重试次数
          max-attempts: 10
          # 重试间隔时间
          initial-interval: 2000ms


#mybatis-plus
mybatis-plus:
  #配置Mapper映射文件
  mapper-locations: classpath:/mappers/*.xml
  # 配置Mybatis数据返回类型别名(默认别名为类名)
  type-aliases-package: com.hanliy.pojo
  configuration:
    # 自动驼峰命名
    map-underscore-to-camel-case: true
    # 打印分析日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

核心类

OrderMqConsumer:监听生产者发送队列:order_queue。

这里有一个报错信息。模拟异常情况的发送,使得消息变为死信,再进行处理。

@Service
public class OrderMqConsumer {
    @Autowired
    private DispatcherService dispatcherService;

    private int num = 1;

    @RabbitListener(queues = {"order_queue"})
    public void messageConsumer(String orderMessage, Channel channel, CorrelationData correlationData,
                                @Header(AmqpHeaders.DELIVERY_TAG) long tags) throws IOException {
        try{
            // 1. 获取到的消息队列的消息
            System.out.println("收到的MQ的消息是:" + orderMessage + ", num = " + num++ );
            OrderDTO orderDTO = JSONUtil.toBean(orderMessage, OrderDTO.class);

            // 2. 派单处理
            dispatcherService.save(orderDTO);

            // 模拟异常
            int errorMessage = 1/0;

            // 执行手动ack
            channel.basicAck(tags,false);
        }catch (Exception e){
            // 无法执行手动ack
                /**
                 * 出现异常的情况下,根据实际的情况进行重发
                 * 重发一次后,丢失还是记录、存库。根据自己的业务去决定。
                 *
                 * @param tags  收到的来自 AMQP.Basic.GetOk or AMQP.Basic.Deliver标签
                 * @param multiple true,拒绝提供的递送标签之前的所有邮件;false仅拒绝提供的交付标签。
                 * @param requeue 如果被拒绝的消息应该被重新查询而不是丢弃,则为true
                 */
                channel.basicNack(tags,false,false);
        }
    }
}

DeadMqConsumer:监听处理死信的队列:dead_order_queue。

如果在这里还出现异常,就手动进行干预。禁止套娃。

@Service
public class DeadMqConsumer {

    @RabbitListener(queues = {"dead_order_queue"})
    public void messageConsumer(String orderMessage, Channel channel, CorrelationData correlationData,
                                @Header(AmqpHeaders.DELIVERY_TAG) long tags) throws IOException {
        try{
            // 1. 获取到的消息队列的消息
            System.out.println("进入死信处理,收到的MQ的消息是:" + orderMessage  );
            OrderDTO orderDTO = JSONUtil.toBean(orderMessage, OrderDTO.class);

            // 2. 派单处理
            // 确保消息不被重复消费,1. 可以设置orderId为主键 2. 使用分布式锁来解决
            dispatcherService.save(orderDTO);
            
            // 执行手动ack:会重发消息,直到重发次数耗尽?
            channel.basicAck(tags,false);

            // int errorMessage = 1/0;
        }catch (Exception e){
            // 人工干预
            System.out.println("等待人工处理。。。。");
            System.out.println("发短信预警");
            System.out.println("把消息转移给数据库存储");
            // 丢弃消息
            channel.basicNack(tags,false,false);
        }
    }
}

再启动消费者服务,查看。消息会因为异常,重试后,发送到指定的队列中等待处理。
【RabbitMQ】学习笔记_第28张图片
再处理完了死信以后,web管理界面如下:
【RabbitMQ】学习笔记_第29张图片

六、MQ监控维护

6.1 内存控制

默认情况下,RabbitMQ使用内存超过40%的时候,会发出内存警告,阻塞所有发布消息的连接,一旦警告解除(例如:服务器paging消息到硬盘或者分发消息到消费者并且确认)服务会恢复正常。

RabbitMQ配置详解:https://www.rabbitmq.com/configure.html

1. 命令行

rabbitmqctl set_vm_memory_high_watermark <fraction>
rabbitmqctl set_vm_memory_high_watermark absolute 50MB

为内存阈值,默认是0.4/2GB。

通过命令行修改阈值在Broker重启之后,会失效。

通过配置文件修改阈值不会随着重启而消息,需要重启Broker才会生效。

此处,调小了磁盘的大小来演示内存警告。
【RabbitMQ】学习笔记_第30张图片
可以看到连接也已经锁定了。

【RabbitMQ】学习笔记_第31张图片

2. 配置文件

当前的配置文件:/etc/rabbitmq/rabbitmq.conf

# 触发流控制的内存阈值。可以是绝对的或相对于操作系统可用的 RAM 量:
# 使用 relative 相对值进行设置fraction,建议在0.4~0.7之间,不建议超过0.7
vm_memory_high_watermark.relative = 0.6
# 使用 absolute 绝对值,单位是KB、MB、GB等。 
vm_memory_high_watermark.absolute = 2 GB

如果你的RAM是8G,设置的相对值0.4,也就是绝对值的3.2GB(8*0.4)。

6.2 磁盘预警

当磁盘的剩余空间地域确定的阈值,RabbitMQ会同样的阻塞生产者。这样可以避免因非持久化的消息持续换页而耗尽磁盘空间,导致服务器崩溃。

在默认情况下,磁盘为50MB会进行预警:存储可持续化序列所在磁盘分区还剩50MB磁盘空间的时候会阻塞生产者,并停止内存消息换页到磁盘的步骤。

这个阈值可以减少,但是不能完全的消除因磁盘耗尽导致的崩溃的可能性。比如在两次磁盘空间的检查间隙内,第一次检查是60MB,第二次检查1MB,就会出现警告。

1. 命令行

rabbitmqctl set_disk_free_limit <disk_limit>
rabbitmqctl set_disk_free_limit_memory_limit <fraction>

其中,

disk_limit:固定单位KB、MB、GB

fraction:相对阈值,建议范围在1.0~2.0之间。(相对于内存)

2. 配置文件

# RabbitMQ 存储数据的分区的磁盘可用空间限制。当可用磁盘空间低于此限制时,将触发流量控制。该值可以相对于 RAM 的总量设置,也可以设置为以字节为单位的绝对值,或者以信息单位(例如“50MB”或“5GB”)为单位:
disk_free_limit.relative = 3.0
disk_free_limit.absolute = 2 GB
# 默认情况下,可用磁盘空间必须超过 50MB。
disk_free_limit.absolute = 50 MB

6.3 内存换页

在某个Broker节点以及内存阻塞生产者之前,他会尝试将队列中的消息换页到磁盘以释放内存,持久化和非持久化的消息都会写入磁盘。其中,持久化的消息本身在磁盘中有一个副本,所以,在转移的过程中持久化的消息会先从内存中消除掉。

在默认情况下,内存到达阈值是50%,就会进行换页处理。

也就是说,在默认情况下,该内存的阈值为0.4情况下,当内存超过了0.2(0.4*0.5),会进行换页动作。

# 设置需小于1,内存到达极限再进行换页意义不大
vm_memory_high_watermark_paging_ratio = 0.5 

附录

1. 报错信息

此处的报错信息就是你修改了已有的队列:direct_ttl_queue

队列在创建后,不可修改。如需修改,需要重新写起一条队列(不推荐,把原队列删除。因为在实际的开发过程中,你打算删除的那条队列或许还在工作)

2022-04-06 15:22:06.724 ERROR 12488 --- [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory       : Shutdown Signal: channel error; protocol method: #method(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-dead-letter-exchange' for queue 'direct_ttl_queue' in vhost '/': received the value 'direct_dead_exchange' of type 'longstr' but current is none, class-id=50, method-id=10)

2. 消费者在接受消息的时候,出现异常,会导致什么样的问题?应该怎么去进行处理?

问题:导致死循环,服务的消息重试投递

解决思路:

  1. 控制重发的次数 + 死信队列
    设置了重试的次数,如果已达到最大的重试次数且没有成功处理,MQ会将消息进行抛弃。
    为了避免消息丢失,我们就需要把消息放入到死信队列中。
  2. try+catch+手动ack+死信队列
    这里有个注意点就是,使用了 try+catch 且 yaml 配置了重试次数:
    • basicNack() 里面参数 requeue 为 true 还是会一直重发。
    • basicNack() 里面参数 requeue 为 false 的话,就不会再根据yaml里面配置的重试次数进行重试,也就是对yaml里面配置的全局重试次数进行覆盖,即:单个配置冲掉全局配置。

  1. 尚硅谷消息中间件课件,提取码:6syv:链接:https://pan.baidu.com/s/18kxeOdxwSUFbyvwMOGfZyg ↩︎

你可能感兴趣的:(Spring,Boot,springboot,rabbitmq)