2. RabbitMQ之Work Queues

文章目录

  • 1. Work Quesues概述
  • 2. Work Queues案例
  • 3. 不公平分发
  • 4. 预期值

1. Work Quesues概述

work queues是工作队列,又叫任务队列,是为了解决多个消费者有序执行密集型的资源任务。比如以下图为例,生产者产生大量消息发给了消息队列中,但是消息消费时比较耗时,这种情况下可以创建多个消费者同时进行消费,但一定要保证一个消息只能被一个消费者进行消费,假如一个消息被Consumer1消费了就不能再被Consumer2或者Consumer3再消费,所以queue队列中消息就像有序的被消费,queue队列中第一个消息分配给了Consumer1进行消费,第二个消息会被分配给Consumer2进行消费,第三个消息被分配给Consumer3进行消费,然后再依次进行消费……,这种现象就是work queues的轮训消费形式。
2. RabbitMQ之Work Queues_第1张图片

2. Work Queues案例

假如一个生产者连续生产了10个消息,3个消费者依次消费这10个消息。

首先创建一个连接RabbitMQ的工具类,生产者和消费者都需要通过这个工具类与RabbitMQ服务器进行连接。

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 RabbitmqUtil {
    private final static String HOST_NM = "192.168.85.100";
    private final static String USER_NM = "admin";
    private final static String PASSWORD = "123";

    public static Connection getConnection() throws IOException, TimeoutException {
        /*创建链接工厂*/
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST_NM);       /*设置rabbitmq服务器地址*/
        factory.setUsername(USER_NM);   /*设置用户名*/
        factory.setPassword(PASSWORD);  /*设置用户密码*/
        /*创建connection链接*/
        Connection connection = factory.newConnection();
        return connection;
    }

    public static Channel getChannel() throws IOException, TimeoutException {
        Connection connection = RabbitmqUtil.getConnection();
        /*创建一个信道*/
        Channel channel = connection.createChannel();
        return channel;
    }
}

接下来创建一个生产者Producer,生产者连续生产10个消息存入消息队列。

import com.lzj.rabbitmq.RabbitmqUtil;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Producer {

    private final static String QUEUE_NAME = "hello_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        /*获取信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*
         * 声明一个队列
         * 1. 第一个参数表示队列的名字
         * 2. 第二个参数表示队列中消息是否要持久化, false表示不持久化存储在内存中, 默认为false
         * 3. 第三个参数表示该队列是否只供一个消费者消费, 不与其它消费者共享, false表示不共享
         * 4. 最后一个消费者断开链接后是否自动删除队列, true表示自动删除
         * 5. 其它参数
         * */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        String message = null;
        for(int i=0; i<10; i++){
            message = "hello world " + i;
            /*
             * 发送消息
             * 1. 发送到哪个交换机
             * 2. 指定路由的key是哪个
             * 3. 其它参数信息
             * 4. 消息体
             * */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("hello world " + i + "消息发送完毕");
        }
    }
}

然后分别创建3个消费者Consumer1、Consumer2、Consumer3,3个消费者的代码一致,比如下面是Consumer1消费者的代码,然后复制2份分别改成Consumer2和Consumer3即可。

import com.lzj.rabbitmq.RabbitmqUtil;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer1 {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException {
        /*获取信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*
         * 消费者消费消息
         * 1. 第一个参数代表消费哪个队列
         * 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
         * */
        /*消息消费时如何消费需回调的接口*/
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("Consumer1消费的消息体为:" + new String(message.getBody()));
        };

        /*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("Consumer1取消消费消息的标签为:" + consumerTag);
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}

通过上面几步,生产者和消费者都已创建好,下面验证一下测试结果,首先分别启动3个消费者程序,由于此时hello_queue队列中还没有消息,所以3个消费者都没有消息进行消费处于等待中。然后启动Producer程序,连续生产了10个消息,如下所示

hello world 0消息发送完毕
hello world 1消息发送完毕
hello world 2消息发送完毕
hello world 3消息发送完毕
hello world 4消息发送完毕
hello world 5消息发送完毕
hello world 6消息发送完毕
hello world 7消息发送完毕
hello world 8消息发送完毕
hello world 9消息发送完毕

然后查看Consumer1消费消息情况,发现Consumer1的终端输出如下所示,表示消费了下面4个消息。

Consumer1消费的消息体为:hello world 0
Consumer1消费的消息体为:hello world 3
Consumer1消费的消息体为:hello world 6
Consumer1消费的消息体为:hello world 9

Consumer2消费了下面3个消息

Consumer2消费的消息体为:hello world 1
Consumer2消费的消息体为:hello world 4
Consumer2消费的消息体为:hello world 7

Consumer3消费了下面3个消息

Consumer3消费的消息体为:hello world 2
Consumer3消费的消息体为:hello world 5
Consumer3消费的消息体为:hello world 8

3个消费者消费的消息总数正好等于生产者生产消息的总数,3个消费者分别依次消费hello_queue队列中的消息,保证了消息轮询被消费,没有被重复消费。

3. 不公平分发

上面的work queues在分发消息时是公平分发的,不管每个消费者消费消息时耗时长短,每个消费者消费消息的数量是相等的。但在实际应用过程中,有些消费者执行速度快,有些消费者执行速度慢,为保证消息能够及时处理,多采用不公平分发,即消费快的能够处理更多消息,消费慢的处理的消息数量少。
如果开启不公平分发标志,只需要在消费者端设置channel.basicQos即可。
比如下面两个消费者,一个消费消息快一个消费慢,下面对比两者消费情况

public class Consumer1 {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException {
        /*创建信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*消息消费时如何消费需回调的接口*/
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("Consumer1正在处理消息:" + new String(message.getBody()));
            try {
                Thread.sleep(1000*3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /*
            * 向服务器发送肯定应答
            * 第一个参数表示消息的标签, 标识消息头的唯一性, 代表要确认哪一个消息
            * 第二个参数表示是否批量确认, false表示不需要批量确认, 就需要每个消息都要手动确认
            * */
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            System.out.println("Consumer1消费成功");
        };
        /*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println(consumerTag);
            System.out.println("消费被取消");
        };

        /*
        * 设置不公平分发标志
        * 1表示不公平分发;默认是0公平分发, 也即轮训分发
        * */
        channel.basicQos(1);
        /*
        * 消费者消费消息
        * 1. 第一个参数代表消费哪个队列
        * 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
        * */
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
    }
}
public class Consumer2 {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException {
        /*创建信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*消息消费时如何消费需回调的接口*/
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("Consumer1正在处理消息:" + new String(message.getBody()));
            try {
                Thread.sleep(1000*10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /*
            * 向服务器发送肯定应答
            * 第一个参数表示消息的标签, 标识消息头的唯一性, 代表要确认哪一个消息
            * 第二个参数表示是否批量确认, false表示不需要批量确认, 就需要每个消息都要手动确认
            * */
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            System.out.println("Consumer2消费成功");
        };
        /*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println(consumerTag);
            System.out.println("消费被取消");
        };

        /*
         * 设置不公平分发标志
         * 1表示不公平分发;默认是0公平分发, 也即轮训分发
         * */
        channel.basicQos(1);
        /*
        * 消费者消费消息
        * 1. 第一个参数代表消费哪个队列
        * 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
        * */
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
    }
}

还是借用上面的Producer生产真,现在分别启动两个消费者,然后启动生产者,首先查看Consumer1的消费情况,输出如下所示

Consumer1正在处理消息:hello world 0
Consumer1消费成功
Consumer1正在处理消息:hello world 2
Consumer1消费成功
Consumer1正在处理消息:hello world 3
Consumer1消费成功
Consumer1正在处理消息:hello world 4
Consumer1消费成功
Consumer1正在处理消息:hello world 6
Consumer1消费成功
Consumer1正在处理消息:hello world 7
Consumer1消费成功
Consumer1正在处理消息:hello world 8
Consumer1消费成功

查看Consumer2消费消息情况,输出如下所示

Consumer1正在处理消息:hello world 1
Consumer2消费成功
Consumer1正在处理消息:hello world 5
Consumer2消费成功
Consumer1正在处理消息:hello world 9
Consumer2消费成功

通过对比结果可以看出,Consumer1和Consumer2不再是公平消费,消费的快的Consumer1消费的消息更多,提升了消息处理效率。
从RabbitMQ服务器浏览器管理端也可以查看消费者是不公平消费的,如下图所示Prefetch Count为1表示两个消费者都是不公平消费。
2. RabbitMQ之Work Queues_第2张图片

4. 预期值

预期值也是不公平分发的一个特例。

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

案例:与前面案例不公平分发的案例一致,只需要分别修改Consumer1和Consumer2的预期值即可

Consumer1设置不公平分发的地方改成预期值,如下所示

public class Consumer1 {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException {
        /*创建信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*消息消费时如何消费需回调的接口*/
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("Consumer1正在处理消息:" + new String(message.getBody()));
            try {
                Thread.sleep(1000*3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /*
            * 向服务器发送肯定应答
            * 第一个参数表示消息的标签, 标识消息头的唯一性, 代表要确认哪一个消息
            * 第二个参数表示是否批量确认, false表示不需要批量确认, 就需要每个消息都要手动确认
            * */
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            System.out.println("Consumer1消费成功");
        };
        /*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println(consumerTag);
            System.out.println("消费被取消");
        };

        /*
        * 设置预期值
        * */
        channel.basicQos(7);
        /*
        * 消费者消费消息
        * 1. 第一个参数代表消费哪个队列
        * 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
        * */
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
    }
}

Consumer2设置不公平分发的地方修改为预期值如下所示

public class Consumer2 {
    private final static String QUEUE_NAME = "hello_queue";
    public static void main(String[] args) throws IOException, TimeoutException {
        /*创建信道*/
        Channel channel = RabbitmqUtil.getChannel();
        /*消息消费时如何消费需回调的接口*/
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("Consumer1正在处理消息:" + new String(message.getBody()));
            try {
                Thread.sleep(1000*10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /*
            * 向服务器发送肯定应答
            * 第一个参数表示消息的标签, 标识消息头的唯一性, 代表要确认哪一个消息
            * 第二个参数表示是否批量确认, false表示不需要批量确认, 就需要每个消息都要手动确认
            * */
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            System.out.println("Consumer2消费成功");
        };
        /*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println(consumerTag);
            System.out.println("消费被取消");
        };

        /*
         * 设置预期值
         * */
        channel.basicQos(3);
        /*
        * 消费者消费消息
        * 1. 第一个参数代表消费哪个队列
        * 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
        * */
        channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
    }
}

通过前面几种案例总结来看,channel.basicQos(0)为公平分发,即轮询;channel.basicQos(1)为不公平分发;channel.basicQos(n),n>1时为预期值。

同样分别运行Consumer1和Consumer2,然后运行Provider,通过终端输出观察情况,Consumer1输出如下所示

Consumer1正在处理消息:hello world 0
Consumer1消费成功
Consumer1正在处理消息:hello world 2
Consumer1消费成功
Consumer1正在处理消息:hello world 4
Consumer1消费成功
Consumer1正在处理消息:hello world 6
Consumer1消费成功
Consumer1正在处理消息:hello world 7
Consumer1消费成功
Consumer1正在处理消息:hello world 8
Consumer1消费成功
Consumer1正在处理消息:hello world 9
Consumer1消费成功

Consumer2输出如下所示

Consumer1正在处理消息:hello world 1
Consumer2消费成功
Consumer1正在处理消息:hello world 3
Consumer2消费成功
Consumer1正在处理消息:hello world 5
Consumer2消费成功

发现符合设置的预期值,Consumer1消费了7个消息,Consumer2消费了3个消息。

参考:
https://blog.csdn.net/Leon_Jinhai_Sun/article/details/120684731
https://www.jianshu.com/p/4d043d3045ca

你可能感兴趣的:(rabbitmq,rabbitmq,Work,Queues,不公平分发,Prefetch,Count,预期值)