3. RabbitMQ 的(Hello World) 和 RabbitMQ 的(Work Queues)工作队列

3. RabbitMQ 的(Hello World) 和 RabbitMQ 的(Work Queues)工作队列

文章目录

  • [3. RabbitMQ 的(Hello World) 和 RabbitMQ 的(Work Queues)工作队列](#3. RabbitMQ 的(Hello World) 和 RabbitMQ 的(Work Queues)工作队列)
  • [1. RabbitMQ 的经典国际代码(Hello World)](#1. RabbitMQ 的经典国际代码(Hello World))
  • [2. RabbitMQ 的 Work Queues(工作队列)](#2. RabbitMQ 的 Work Queues(工作队列))
    • [2.1 RabbitMQ 的 轮训接受/消费消息](#2.1 RabbitMQ 的 轮训接受/消费消息)
    • [2.2 RabbitMQ 的 消息应答](#2.2 RabbitMQ 的 消息应答)
    • [2.3 RabbitMQ 的 持久化](#2.3 RabbitMQ 的 持久化)
      • [2.3.1 RabbitMQ 当中队列的持久化:](#2.3.1 RabbitMQ 当中队列的持久化:)
      • [2.3.2 RabbitMQ 消息的持久化:](#2.3.2 RabbitMQ 消息的持久化:)
    • [2.4 RabbitMQ 的 不公平分发(简单的说类似于权重分发)](#2.4 RabbitMQ 的 不公平分发(简单的说类似于权重分发))
    • [2.5 RabbitMQ 当中的"预取值" 设置](#2.5 RabbitMQ 当中的“预取值” 设置)
  • [3. 最后:](#3. 最后:)

1. RabbitMQ 的经典国际代码(Hello World)

我们将用 Java 编写两个程序。发送单个消息的生产者和接收消息并打印 出来的消费者。我们将介绍 Java API 中的一些细节。

在下图中," P"是我们的生产者," C"是我们的消费者。中间的框是一个队列-RabbitMQ 代 表使用者保留的消息缓冲区

创建一个 rabbitmq-hello Module 模块,导入 RabbitMQ 的相关依赖在 pom.xml 文件中。添加后记得刷新一下 Maven

xml 复制代码
    <!--指定 jdk 编译版本-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <!--rabbitmq 依赖客户端-->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.8.0</version>
        </dependency>
        <!--操作文件流的一个依赖-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>

创建一个生产者发送消息:

java 复制代码
package com.rainbowsea.rabbitmq.one;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 生产者,发送消息
 */
public class Producer {
    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "hello";


    // 生产者,发送消息
    public static void main(String[] args) throws IOException, TimeoutException {

        // 创建一个连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 工厂IP连接 RabbitMQ的队列
        connectionFactory.setHost("192.168.76.156"); // Rabbitmq 所在的IP地址
        // 连接 RabbitMQ 的用户名
        connectionFactory.setUsername("admin");
        // 连接 RabbitMQ 的密码
        connectionFactory.setPassword("123");
        // 创建连接
        Connection connection = connectionFactory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();
        /*
        生成一个队列
        1.队列名称
        2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
        3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
        4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
        5.其它参数

         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        // 发消息
        String message = "Hello World"; // 初次发送消息
        /*
        发送一个消费
        1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
        2.路由的Key值是哪个,本次是队列的名字
        3. 其它参数信息
        4. 发送消息的消息体,要转换为 bit 流进行处理发送
         */
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        System.out.println("消息发送完毕");

    }
}

我们打开浏览器:查看 RabbitMQ 的图形化管理页面:http://192.168.76.156:15672/#/queues

创建一个 Consumer 类作为消费者------> 读取/消费,该名为 hello 队列当中的消息信息。

java 复制代码
package com.rainbowsea.rabbitmq.one;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;


import java.io.IOException;
import java.util.concurrent.TimeoutException;


/**
 * 消费者读取消息,
 * 消费消息
 */
public class Consumer {

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


    // 生产者,发送消息
    public static void main(String[] args) throws IOException, TimeoutException {

        // 创建一个连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 工厂IP连接 RabbitMQ的队列
        connectionFactory.setHost("192.168.76.156"); // Rabbitmq 所在的IP地址
        // 连接 RabbitMQ 的用户名
        connectionFactory.setUsername("admin");
        // 连接 RabbitMQ 的密码
        connectionFactory.setPassword("123");
        // 创建连接
        Connection connection = connectionFactory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();


        // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("读取到的消息" + message);
            String messages = new String(message.getBody());
            System.out.println("读取到的消息" + messages);
        };

        // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println("没有读取到的消息,消息在读取过程中中断了");
        };


        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);


    }
}

2. RabbitMQ 的 Work Queues(工作队列)

Work Queues(工作队列又称任务队列): 的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。

2.1 RabbitMQ 的 轮训接受/消费消息

在这个案例中我们会启动两个工作线程,一个消息发送线程,我们来看看他们两个工作线程 是如何工作的。

这里我们将一个获取频道的工具抽取出来作为一个工具类使用。

java 复制代码
package com.rainbowsea.rabbitmq.utils;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;


/**
 * 此类为连接工厂创建信道的工具类
 */
public class RabbitMQUtils {


    /**
     * 得到一个连接的 channel
     * @return
     * @throws IOException
     * @throws TimeoutException
     */
    public static Channel getChannel() throws IOException, TimeoutException {
        // 创建一个连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 工厂IP连接 RabbitMQ的队列
        connectionFactory.setHost("192.168.76.156"); // Rabbitmq 所在的IP地址
        // 连接 RabbitMQ 的用户名
        connectionFactory.setUsername("admin");
        // 连接 RabbitMQ 的密码
        connectionFactory.setPassword("123");
        // 创建连接
        Connection connection = connectionFactory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();

        return channel;
    }
}

启动一个发送线程(作为生产者,发送消息)

java 复制代码
package com.rainbowsea.rabbitmq.two;


import com.rabbitmq.client.Channel;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

/**
 * 生产者发送消息
 */
public class Task01 {

    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "hello";


    /**
     * 发送大量消息
     *
     * @param args
     */
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtils.getChannel();


        /*
        生成一个队列
        1.队列名称
        2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
        3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
        4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
        5.其它参数

         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 从控制台当中接受消息
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            String message = scanner.next();
            /*
        发送一个消费
        1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
        2.路由的Key值是哪个,本次是队列的名字
        3. 其它参数信息
        4. 发送消息的消息体,要转换为 bit 流进行处理发送
         */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("发送消息完成:" + message);
        }

    }
}

启动两个工作线程(作为消费者,读取/消费信息)

java 复制代码
package com.rainbowsea.rabbitmq.two;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Worker01 {

    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "hello";

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

        // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("读取到的消息" + new String(message.getBody()));
        };

        // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
        };


        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */
        System.out.println("C1 等待接收消息");
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}
java 复制代码
package com.rainbowsea.rabbitmq.two;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Worker02 {

    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "hello";

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

        // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("读取到的消息" + new String(message.getBody()));
        };

        // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
        };


        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */
        System.out.println("C2 等待接收消息");
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}

运行执行效果:

  • 启动两个工作线程(两个消费者,读取信息)
  • 启动一个发送线程(生产者,发送消息)

通过程序执行发现生产者总共发送 4 个消息,消费者 1 和消费者 2 分别分得两个消息,并且 是按照有序的一个接收一次消息。

2.2 RabbitMQ 的 消息应答

消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分任务,就突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。

为了保证消息在发送过程中不丢失,RabbitMQ 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 RabbitMQ 它已经处理了,RabbitMQ 可以把该消息删除了

自动应答:

消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡 ,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢 失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制 , 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终 使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并 以某种速率能够处理这些消息的情况下使用。

消息应答的方法:

sh 复制代码
A.Channel.basicAck(用于肯定确认)
     RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
B.Channel.basicNack(用于否定确认)
C.Channel.basicReject(用于否定确认)
    与 Channel.basicNack 相比少一个参数 
    不处理该消息了直接拒绝,可以将其丢弃了

Multiple 的解释:

手动应答的好处是可以批量应答并且减少网络拥堵

multiple 的 true 和 false 代表不同意思

true 代表批量应答 channel 上未应答的消息

比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时 5-8 的这些还未应答的消息都会被确认收到消息应答

false 同上面相比

只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答

消息自动重新入队:

如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息 未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者 可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确 保不会丢失任何消息。

消息手动应答代码:

认消息采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改 为手动应答,消费者在上面代码的基础上增加下面画红色部分代码。

java 复制代码
 // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
            System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));

            /*
            手动应答
            1. 消息的标记 tag
            2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */
        System.out.println("C1 等待接收消息");
        boolean autoAck = false;  // 采用手动应答的方式
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);

注意:手动应答的编写是写在,消费者/读取者的,读取成功的 DeliverCallback 回调函数当中的,其中的,后面的 channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback); 第二个参数要改为 false 表示,启动手动应答的方式

这里我们额外编写一个,睡眠的工具类,用于模仿消费者读取/消费RabbitMQ 消息队列当中的信息的一个网络延时。如下:

java 复制代码
package com.rainbowsea.rabbitmq.utils;


/**
 * 睡眠工具类
 */
public class SleepUtils {
    public static void sleep(int second) {
        try {
            Thread.sleep(1000 * second);  // 单位毫秒
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }


    }
}

编写两个消费者/读取 RabbitMQ 消息队列当中的消息。一个消费者,模拟延时 1s ,另外一个消费者模拟延时30s

一个消费者,模拟延时 1 S

java 复制代码
package com.rainbowsea.rabbitmq.three;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
import com.rainbowsea.rabbitmq.utils.SleepUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Worker03 {

    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtils.getChannel();
        System.out.println("C1 等待接收消息处理时间较短");

        // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
        };

        // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
            System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));

            //  读取时,睡眠 1 s
            SleepUtils.sleep(1);

            /*
            手动应答
            1. 消息的标记 tag
            2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */

        boolean autoAck = false;  // 采用手动应答的方式
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

一个消费者,模拟延时 30 S

java 复制代码
package com.rainbowsea.rabbitmq.three;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
import com.rainbowsea.rabbitmq.utils.SleepUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Worker04 {

    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "ack_queue";

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


        System.out.println("C2 等待接收消息处理时间较长");

        // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
        };

        // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
            System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));
            //  读取时,睡眠 30 s
            SleepUtils.sleep(30);

            /*
            手动应答
            1. 消息的标记 tag
            2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */

        boolean autoAck = false;  // 采用手动应答的方式
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

生产者,发送消息

手动应答,生产者,不需要做出什么修改上的操作,就是发送消息,让消费者读取/消费

java 复制代码
package com.rainbowsea.rabbitmq.three;


import com.rabbitmq.client.Channel;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

/**
 * 消息在手动改应答时是不丢失,返回队列当中重新应答
 */
public class Task2 {
    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "ack_queue";


    /**
     * 发送大量消息
     *
     * @param args
     */
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtils.getChannel();


        /*
        生成一个队列
        1.队列名称
        2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
        3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
        4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
        5.其它参数

         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 从控制台当中接受消息
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            String message = scanner.next();
            /*
        发送一个消费
        1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
        2.路由的Key值是哪个,本次是队列的名字
        3. 其它参数信息
        4. 发送消息的消息体,要转换为 bit 流进行处理发送
         */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("发送消息完成:" + message);
        }

    }
}

运行执行结果:

正常情况下消息发送方发送两个消息 C1 和 C2 分别接收到消息并进行处理

这里:我们的发送者发送消息 dd,发出消息之后的时候,我们就把(C2 消费者处理时间 30 S 比较长的停止) ,按照 RabbitMQ 默认的轮询读取的操作,这时候的 dd 应该是由 C2 消费者/读取处理该消息的,但是由于这个时候 C2 30s 的延时,并没有将 dd 消息处理掉,就被我们停止了,此时,我们发现的结果运行结果就是,该消息 dd 被 C1 接收到了,这就说明:手动应答,因为 C2 处理 dd消息的时候被中途停止了,并没有将该 dd消息处理掉,所有没有向 RabbitMQ 消息队列作出一个应答的操作,那么该消息就没有被 RabbitMQ 删除掉,而是把 dd 这个消息交给分配给了 C1 消费者处理了。

特别说明:

  • 这里我们采用的时 手动应答 的方式,它是在一个RabbitMQ 消息队列的一个默认的轮询读取 的方式下的。
  • 手动应答: 实现的效果就是:对应某个消费者,因为某种原因(网络延时),没有及时将,它该在轮询读取的情况下,并没有将所有的消息给读取完,因为没有读取完,又因为采用的是一个 手动应答的方式,队列知道它没有将,该队列当中的消息读取完,所有 RabbitMQ 消息队列,不会将该消息删除,而是等到该消费者 将该队列的内容读取完才会,删除该队列当中的内容。
  • 特别注意:我们这里的两个消费者,如果一开始在,RabbitMQ 没有声明对应的 channel信道,队列信息,那么需要先启动生产者,不然消费者,找不到对应,队列信息,是会报错,无法运行的。

2.3 RabbitMQ 的 持久化

概念:

刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当 RabbitMQ 服务停掉之后,消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息。除非告知它不要这样做,确保消息不会丢失需要两件事:我们需要将队列和消息都标记为持久化

2.3.1 RabbitMQ 当中队列的持久化:

之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果 要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化

java 复制代码
 /*
        生成一个队列
        1.队列名称
        2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
        3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
        4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
        5.其它参数

         */
        boolean durable = true;
        channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误。原因:就是无法将一个一开始不是持久化的队列修改为持久化的队列

我们需要将该队列删除,重新生成一个队列,让其一开始就是一个持久化的队列。

补充:

以下为控制台中持久化与非持久化队列的 UI 显示区:


这个时候即使重启 rabbitmq 队列也依然存在
注意:上面我们仅仅只是将 RabbitMQ 当中的队列进行了持久化,但是其中 RabbitMQ 当中的消息是没有被持久化的。

2.3.2 RabbitMQ 消息的持久化:

要想让消息实现持久化需要在生产者 发送者,修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添 加这个属性。

java 复制代码
   /*
        发送一个消费
        1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
        2.路由的Key值是哪个,本次是队列的名字
        3. 其它参数信息
        4. 发送消息的消息体,要转换为 bit 流进行处理发送
         */

            channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
            System.out.println("发送消息完成:" + message);

将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是 这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没 有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要 更强有力的持久化策略,参考该博客的后续章节。

2.4 RabbitMQ 的 不公平分发(简单的说类似于权重分发)

在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是 很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间 处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是 RabbitMQ 并不知道这种情况它依然很公平的进行分发。

为了避免这种情况,我们可以设置参数 channel.basicQos(1);

java 复制代码
     // 设置不公平分发
        int prefetchCount = 1;
        channel.basicQos(prefetchCount);

补充:


意思就是如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理一个 任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完 成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加 新的 worker 或者改变其他存储任务的策略。

运行测试:延续使用上面我们 RabbitMQ 手动应答的代码,仅仅只修改其中的消费者的channel.basicQos(prefetchCount); 参数 ,生产者不需要修改。

java 复制代码
package com.rainbowsea.rabbitmq.three;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
import com.rainbowsea.rabbitmq.utils.SleepUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Worker03 {

    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtils.getChannel();
        System.out.println("C1 等待接收消息处理时间较短");

        // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
        };

        // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
            System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));

            //  读取时,睡眠 1 s
            SleepUtils.sleep(1);

            /*
            手动应答
            1. 消息的标记 tag
            2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */

        // 设置不公平分发
        int prefetchCount = 1;
        channel.basicQos(prefetchCount);

        boolean autoAck = false;  // 采用手动应答的方式
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}
java 复制代码
package com.rainbowsea.rabbitmq.three;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

/**
 * 消息在手动改应答时是不丢失,返回队列当中重新应答
 */
public class Task2 {
    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "ack_queue";


    /**
     * 发送大量消息
     *
     * @param args
     */
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtils.getChannel();


        /*
        生成一个队列
        1.队列名称
        2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
        3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
        4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
        5.其它参数

         */
        boolean durable = true;
        channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

        // 从控制台当中接受消息
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            String message = scanner.next();
            /*
        发送一个消费
        1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
        2.路由的Key值是哪个,本次是队列的名字
        3. 其它参数信息
        4. 发送消息的消息体,要转换为 bit 流进行处理发送
         */

            channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
            System.out.println("发送消息完成:" + message);
        }

    }
}

运行结果:

2.5 RabbitMQ 当中的"预取值" 设置

本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息另外来自消费 者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此 缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题 。这个时候就可以通过使用 basic.qos 方法设 置"预取计数"值来完成的。该值定义通道上允许的未确认消息的最大数量 。一旦数量达到配置的数量, RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有 未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何 消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知 这个情况到并再发送一条消息。消息应答和 QoS 预取值对用户吞吐量有重大影响。通常,增加预取将提高 向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的 RAM 消耗(随机存取存储器)应该小心使用具有无限预处理 的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的 内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范 围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为 1 是最保守的。当然这 将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境 中。对于大多数应用来说,稍微高一点的值将是最佳的。

**该阈值设置和,不公平分发设置是一样的,编码是一样的,不同的是该设置的值,不是 1了,而是具体的一个该消费者具体要/能处理的"阈值"了 **

java 复制代码
     // 设置预取值为: 2
        int prefetchCount = 2;
        channel.basicQos(prefetchCount);
java 复制代码
  // 设置预取值为: 5
        int prefetchCount = 5;
        channel.basicQos(prefetchCount);
java 复制代码
package com.rainbowsea.rabbitmq.three;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
import com.rainbowsea.rabbitmq.utils.SleepUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Worker04 {

    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "ack_queue";

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


        System.out.println("C2 等待接收消息处理时间较长");

        // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
        };

        // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
            System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));
            //  读取时,睡眠 30 s
            SleepUtils.sleep(30);

            /*
            手动应答
            1. 消息的标记 tag
            2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */

        // 设置不公平分发
        //int prefetchCount = 1;
        // 设置预取值为: 5
        int prefetchCount = 5;
        channel.basicQos(prefetchCount);

        boolean autoAck = false;  // 采用手动应答的方式
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

运行测试:延续使用上面我们 RabbitMQ 手动应答的代码,仅仅只修改其中的消费者的int prefetchCount = 5; channel.basicQos(prefetchCount); 参数 ,生产者不需要修改。

java 复制代码
package com.rainbowsea.rabbitmq.three;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

/**
 * 消息在手动改应答时是不丢失,返回队列当中重新应答
 */
public class Task2 {
    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "ack_queue";


    /**
     * 发送大量消息
     *
     * @param args
     */
    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtils.getChannel();


        /*
        生成一个队列
        1.队列名称
        2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
        3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
        4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
        5.其它参数

         */
        boolean durable = true;
        channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

        // 从控制台当中接受消息
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            String message = scanner.next();
            /*
        发送一个消费
        1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
        2.路由的Key值是哪个,本次是队列的名字
        3. 其它参数信息
        4. 发送消息的消息体,要转换为 bit 流进行处理发送
         */

            channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
            System.out.println("发送消息完成:" + message);
        }

    }
}
java 复制代码
package com.rainbowsea.rabbitmq.three;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
import com.rainbowsea.rabbitmq.utils.SleepUtils;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Worker03 {

    // ctrl + shift + u 大写转换
    // 队列名称
    public static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Channel channel = RabbitMQUtils.getChannel();
        System.out.println("C1 等待接收消息处理时间较短");

        // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
        };

        // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
            System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));

            //  读取时,睡眠 1 s
            SleepUtils.sleep(1);

            /*
            手动应答
            1. 消息的标记 tag
            2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        /*
        消费者消费/读取消息
        1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
        2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
        3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
        4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
         */

        // 设置不公平分发
        //int prefetchCount = 1;

        // 设置预取值为: 2
        int prefetchCount = 2;
        channel.basicQos(prefetchCount);

        boolean autoAck = false;  // 采用手动应答的方式
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

运行结果:

我们可以看到

虽然我们这里C1 设置了预取值:是读取2 个,但是这里它却读取到了4个

,是因为,它的预取值的计算是从,它是实时更新的:比如一开始处理了2个

,那么就是0个,那它就是可以再处理 2个消息,主要是这个 C1 处理得太快了

虽然 C2 设置的是处理 5 个但是处理的太慢了

,由于 C1 处理的太快了,将C2 的处理的消息

抢走了,所以预取值,不是绝对的,而是相对的。

3. 最后:

"在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。"

相关推荐
mghio9 小时前
Dubbo 中的集群容错
java·微服务·dubbo
Asthenia04129 小时前
Spring AOP 和 Aware:在Bean实例化后-调用BeanPostProcessor开始工作!在初始化方法执行之前!
后端
Asthenia041210 小时前
什么是消除直接左递归 - 编译原理解析
后端
Asthenia041210 小时前
什么是自上而下分析 - 编译原理剖析
后端
Asthenia041210 小时前
什么是语法分析 - 编译原理基础
后端
Asthenia041210 小时前
理解词法分析与LEX:编译器的守门人
后端
uhakadotcom10 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
Asthenia041211 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz96512 小时前
ovs patch port 对比 veth pair
后端