RabbitMQ非官方教程(三)工作队列

上个教程的Demo中,我们编写了程序来发送和接收来自命名队列的消息。在这一部分中,我们将创建一个工作队列,该队列将用于在多个工作人员之间分配耗时的任务。

工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反,我们安排任务在以后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作人员时,任务将在他们之间共享。

这是本节代码地址:https://gitee.com/mjTree/javaDevelop/tree/master/testDemo

制备过程

之前发送了一条包含“ Hello World!”的消息。现在我们将发送代表复杂任务的字符串,假装处理过程时间较长(使用Sleep函数来伪造它)。新建一个Secod包,里面包含NewTask.java和Work.java。

我们将稍微修改上一个示例中的Send.java代码,以允许从命令行发送任意消息。该程序会将任务调度到我们的工作队列中,因此将其命名为NewTask.java。注释的是因为官方想在命令行编译并运行文件,这里就不用那种方式了。

package com.mytest.rabbitMQ.Second;

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

public class NewTask {
    private final static String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel()) {
            channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
            //String message = String.join(" ", argv);  //命令行编译执行时添加参数的方式
            String message = "No.1 message";
            channel.basicPublish("", TASK_QUEUE_NAME,
                    null,
                    message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

上面代码执行完之后在把message中的"No.1"改成"No.2"和"No.3"各运行一次,这样我们队列中存放了3条消息。

package com.mytest.rabbitMQ.Second;

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

public class Work {
    private final static String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");

            System.out.println(" [x] Received '" + message + "'");
            try {
                doWork(message);
            } finally {
                System.out.println(" [x] Done");
            }
        };
        boolean autoAck = true;     // 确认内容如下
        channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) {
        for (char ch: task.toCharArray()) {
            if (ch == '.') {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException _ignored) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

然后我们编写Work代码,然后执行。每次读取一条消息会模拟消费者在很忙的处理消息并花费1s的时间。

循环调度

使用任务队列的优点之一是能够轻松并行化工作。如果我们正在积压工作,我们可以增加更多的工人,这样就可以轻松扩展。首先,让我们尝试同时运行两个消费者程序。他们俩都将从队列中获取消息,但是究竟如何呢?

由于IDEA不能像命令行那样打开多个窗口去执行Work类模拟该过程,这里暂时不演示了。使用官网的演示结果说明过程。

# 首先执行5次NewTask使得队列存放5条消息

java -cp $CP NewTask First message.
# => [x] Sent 'First message.'
java -cp $CP NewTask Second message..
# => [x] Sent 'Second message..'
java -cp $CP NewTask Third message...
# => [x] Sent 'Third message...'
java -cp $CP NewTask Fourth message....
# => [x] Sent 'Fourth message....'
java -cp $CP NewTask Fifth message.....
# => [x] Sent 'Fifth message.....'
# 下面是打开的第一个Work消费者执行的结果
java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'First message.'
# => [x] Received 'Third message...'
# => [x] Received 'Fifth message.....'


# 下面是打开的第二个Work消费者执行的结果
java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'Second message..'
# => [x] Received 'Fourth message....'

默认情况下,RabbitMQ将每个消息依次发送给下一个使用者。换句话说每个消费者都应该会收到相同数量的消息,这种分发消息的方式称为循环

消息确认

执行任务可能需要几秒钟。您可能想知道,如果其中一个使用者开始一项漫长的任务而仅部分完成而死掉,会发生什么情况。使用我们当前的代码,RabbitMQ一旦向消费者发送了一条消息,便立即将其标记为删除。在这种情况下,如果您杀死一个工人,我们将丢失正在处理的消息。我们还将丢失所有发送给该特定工作人员但尚未处理的消息。

但是我们不想丢失任何任务。如果一个消费者进程死亡,我们希望将任务交付给另一个消费者。为了确保消息永不丢失,RabbitMQ支持 消息确认。消费者发送回一个确认(告知),告知RabbitMQ特定的消息已被接收和处理,之后RabbitMQ便可以自由删除该消息。如果消费者进程死了(其通道已关闭,连接已关闭或TCP连接丢失)而没有发送确认,RabbitMQ将了解消息未完全处理,并将重新排队。如果同时有其他消费者在线,它将很快将其重新分发给另一个消费者。这样,您可以确保即使消费者进程偶尔死亡也不会丢失任何消息。没有任何消息超的时候,消费者进程死亡时,RabbitMQ将重新传递消息。即使处理一条消息花费非常非常长的时间也没关系。

首先我们先执行NewTask四次得到4条消息,之后修改Work的代码。

package com.mytest.rabbitMQ.Second;

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

public class Work {
    private final static String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        channel.basicQos(1);    // 一次仅接受一条未经确认的消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");

            System.out.println(" [x] Received '" + message + "'");
            try {
                doWork(message);
            } finally {
                System.out.println(" [x] Done");
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        boolean autoAck = false;     // 确认内容如下
        channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) {
        for (char ch: task.toCharArray()) {
            if (ch == '.') {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException _ignored) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

默认情况下,手动确认消息处于打开状态。在前面的示例中,我们通过autoAck=true标志显式关闭了它们。现在,是时候将该标志设置为false,在完成任务后从消费者那里发送适当的确认。

然后在执行Work过程中对其中断(点击红色按钮强制结束进程),此时消费者接受了第三条消息但还是还在处理突然中断。

以前代码,消息队列是会删除第三条消息,但加上消息确认则不会出现这种消息丢失现象。去网页查看队列中的消息数还有2条,说明我们的消息确认起到效果了。

使用此代码,我们可以确保,即使您在处理消息时终止消费者进程,也不会丢失任何信息。消费者进程死亡后不久,所有未确认的消息将重新发送。

确认必须在收到交货的同一通道上发送。尝试使用其他通道进行确认将导致通道级协议异常。请参阅有关确认文档指南 以了解更多信息。

 

讯息持久性

我们已经学会了如何确保即使消费者进程死亡,消息也不会丢失。但如果RabbitMQ服务器停止,我们的消息仍然会丢失。RabbitMQ退出或崩溃时,它将忘记队列和消息,除非您告知不要这样做。要确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久。

boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);

上面代码本身是正确的,但当前的设置中将无法使用。因为我们在上一节的demo中已经定义了一个名为hello的队列 ,并且默认它不持久,而RabbitMQ不允许您使用不同的参数重新定义现有队列,也就是不能去修改已经定义过的队列属性。但是有一个快速的解决方法-让我们声明一个名称不同的队列去进行操作。

如果还是使用原来的队列task_queue,我们需要去网页把它删除,在网页上,直接点击队列表格中某个队列名称,然后跳转到新页面会有一个"delete queue"按钮,点击就直接删除并自动跳回原来页面。

// 需要对NewTask.java和Work.java中队列声明注释并修改
/*注释前*/
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);



/*注释后*/
 // 声明持久化
boolean durable = true;
//channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);

上面代码保证了rabbitmq重启也不会丢失队列,接下来接着修改代码保证消息不会丢失。我们需要将消息标记为持久性-通过将MessageProperties(实现BasicProperties)设置为PERSISTENT_TEXT_PLAIN的值即可。详细代码在最下面提供

// 把NewTask中消息的某个属性值修改科技

/*修改前*/
channel.basicPublish("", TASK_QUEUE_NAME,
                    null,
                    message.getBytes("UTF-8"));

/*修改后*/
channel.basicPublish("", TASK_QUEUE_NAME,
                    MessageProperties.PERSISTENT_TEXT_PLAIN,
                    message.getBytes("UTF-8"));

有关消息持久性的说明

将消息标记为持久性并不能完全保证不会丢失消息。尽管它告诉RabbitMQ将消息保存到磁盘,但是RabbitMQ接受消息并且尚未保存消息时,还有很短的时间。而且,RabbitMQ不会对每条消息都执行 fsync(2)-它可能只是保存到缓存中,而没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果您需要更强有力的保证,则可以使用 发布者确认

 

公平派遣

您可能已经注意到,调度仍然无法完全按照我们的要求进行。例如,在有两个消费者进程工作的情况下,所有操作繁琐的任务交个A消费者,所有简单的任务交给B消费者,则就使得A消费者将一直忙碌而B消费者几乎没什么任何工作。RabbitMQ对此一无所知,并且仍将平均分配消息。发生这种情况是因为RabbitMQ在消息进入队列时才调度消息。它不会查看使用者的未确认消息数,它只是盲目地将每第n条消息发送给第n个使用者。

为了克服这一点,我们可以使用basicQos方法。这告诉RabbitMQ一次不要给消费者特定数目以上的消息。如果我们设置为1,在处理并确认上一条消息之前,不要将新消息发送给该消费者,而是将其分派给不忙的下一个消费者。这个设置方式在上面代码中测试消费者进程中断时用到了。

关于队列大小的注意事项

如果所有消费者都忙,您的队列就满了。您将需要留意这一点,也许需要增加更多的消费者,或用其他一些策略。

整合持久化和公平派发功能代码

package com.mytest.rabbitMQ.Second;

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


public class NewTask {
    private final static String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");

        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel()) {
            // 声明持久化
            boolean durable = true;
            //channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
            channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);

            //String message = String.join(" ", argv);  //命令行编译执行时添加参数的方式
            String message = "No.1 message";

            channel.basicPublish("", TASK_QUEUE_NAME,
                    MessageProperties.PERSISTENT_TEXT_PLAIN,
                    message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}
package com.mytest.rabbitMQ.Second;

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

public class Work {
    private final static String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        boolean durable = true;
        //channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
        channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        int prefetchCount = 1;
        channel.basicQos(prefetchCount);    // 一次仅接受一条未经确认的消息

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
            try {
                doWork(message);
            } finally {
                System.out.println(" [x] Done");
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        boolean autoAck = false;     // 确认内容如下
        channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) {
        for (char ch: task.toCharArray()) {
            if (ch == '.') {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException _ignored) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

 

使用消息确认basicQos可以设置工作队列。持久化可以使重新启动RabbitMQ也可以使任务继续存在。

有关Channel方法和MessageProperty的更多信息,您可以在线浏览 JavaDocs

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值