04-rabbitmq-工作队列-spring

04-rabbitmq-工作队列-spring

【博文总目录>>>】


【工程下载>>>】


先决条件


本教程假定RabbitMQ已在标准端口(5672)上的localhost上安装并运行。如果使用不同的主机,端口或凭据,连接设置将需要调整。
工作队列(使用spring-ampq客户端)

这里写图片描述

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

工作队列(又称:任务队列)背后的主要思想是避免立即执行资源密集型,并且必须等待完成的任务。相反,我们安排任务在后续完成。我们将任务封装成 消息,并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当你运行很多工作进程时,这些任务将在它们之间共享。

这个概念在Web应用程序中特别有用,尤其适用在短时间HTTP请求窗口中无法处理复杂的任务。

准备


在本教程的前面部分,我们发送了一个包含“Hello World!”的消息。现在我们将发送的的字符串代表复杂任务。我们没有一个现实世界的任务,比如图像被调整大小,或者是要渲染的pdf文件来模拟,所以假设我们很忙 - 通过使用Thread.sleep()函数来假冒它。我们将把字符串中的点数作为其复杂度; 每个点都将占“工作”的一秒钟。例如,由Hello …描述的假任务将需要三秒钟。

如果您尚未设置项目,请参阅第一个教程中的设置。我们将遵循与第一个教程中相同的模式:1)创建一个包(com.example.rabbitmq)并创建一个Tut2Config,Tut2Receiver和Tut2Sender。首先创建一个新包(com.example.rabbitmq),我们将放置我们的三个类。在配置类中,我们设置两个配置文件,教程的标签(“tut2”)和模式的名称(“work-queues”)。我们利用spring来将队列暴露为一个bean。我们将接收者设置为配置文件,并定义两个bean,以对应于上图中的工作进程; receiver1和receiver2。最后,我们定义发件者的配置文件并定义发件者bean。配置现在完成了。

package com.example.rabbitmq;

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

/**
 * RabbitMQ消息配置
 * Author: 王俊超
 * Date: 2017-06-17 10:26
 * All Rights Reserved !!!
 */
@Profile({"tut2", "work-queues"})
@Configuration
public class Tut2Config {
    /**
     * 创建一个消息队列
     * @return
     */
    @Bean
    public Queue hello() {
        return new Queue("hello");
    }

    /**
     * 定一个接收配置对象,定义两个接收者
     */
    @Profile("receiver")
    private static class ReceiverConfig {

        @Bean
        public Tut2Receiver receiver1() {
            return new Tut2Receiver(1);
        }

        @Bean
        public Tut2Receiver receiver2() {
            return new Tut2Receiver(2);
        }
    }

    /**
     * 定义一个消息发送对象
     *
     * @return
     */
    @Profile("sender")
    @Bean
    public Tut2Sender sender() {
        return  new Tut2Sender();
    }
}

发送者


我们将修改发件者,以提供一种方法,通过使用RabbitTemplate的convertAndSend方法发布消息,以非常有创意的方式向邮件中添加点来识别其是否运行更长的任务。convertAndSend即“将Java对象转换为Amqp消息并将其发送到使用默认路由键的默认交换队列中”。

package com.example.rabbitmq;

import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;

/**
 * 消息发送者
 *
 * Author: 王俊超
 * Date: 2017-06-17 10:31
 * All Rights Reserved !!!
 */
public class Tut2Sender {

    /** 消息模板对象 */
    @Autowired
    private RabbitTemplate template;

    /** 消息队列 */
    @Autowired
    private Queue queue;

    private int dots = 0;
    private int count = 0;

    /**
     * 消息发送方法,初始延迟0.5秒,之后每1秒发送一个消息
     */
    @Scheduled(fixedDelay = 1000, initialDelay = 500)
    public void send() {
        StringBuilder builder = new StringBuilder("Hello");
        if (dots++ == 3) {
            dots = 1;
        }

        for (int i = 0; i < dots; i++) {
            builder.append('.');
        }

        builder.append(Integer.toString(++count));
        String message = builder.toString();
        template.convertAndSend(queue.getName(), message);
        System.out.println( " [x] Sent '" + message + "'");
    }
}

接收者


我们的接收器Tut2Receiver模拟了doWork()方法中的假任务的任意长度,其中点数转换为工作所需的秒数。再次,我们在“hello”队列和@RabbitHandler上使用@RabbitListener来接收消息。消费消息的实例被添加到我们的监视器中,以显示哪个实例,消息和处理消息的时间长度。

package com.example.rabbitmq;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.util.StopWatch;

/**
 * 消息接收者对象,并指定接收的消息队列
 * <p>
 * Author: 王俊超
 * Date: 2017-06-17 10:30
 * All Rights Reserved !!!
 */
@RabbitListener(queues = "hello")
public class Tut2Receiver {

    private final int instance;


    public Tut2Receiver(int i) {
        this.instance = i;
    }

    /**
     * 指定消息接收后的处理方法,因为发送的消息是string类型,所以接收方法的入参也是string类型
     *
     * @param in
     * @throws InterruptedException
     */
    @RabbitHandler
    public void receive(String in) throws InterruptedException {
        StopWatch watch = new StopWatch();
        watch.start();
        System.out.println("instance " + this.instance + " [x] Received '" + in + "'");
        doWork(in);
        System.out.println("instance " + this.instance + " [x] Done in " + watch.getTotalTimeSeconds() + "s");
    }

    private void doWork(String in) throws InterruptedException {
        for (char ch : in.toCharArray()) {
            if (ch == '.') {
                Thread.sleep(1000);
            }
        }
    }
}

运行


先运行接收者,需要添加运行参数:–spring.profiles.active=hello-world,receiver
再运行发送者,需要添加运行参数:–spring.profiles.active=hello-world,sender
发件者的输出应该如下所示:

Ready ... running for 10000ms
 [x] Sent 'Hello.1'
 [x] Sent 'Hello..2'
 [x] Sent 'Hello...3'
 [x] Sent 'Hello.4'
 [x] Sent 'Hello..5'
 [x] Sent 'Hello...6'
 [x] Sent 'Hello.7'
 [x] Sent 'Hello..8'
 [x] Sent 'Hello...9'
 [x] Sent 'Hello.10'

而工作进程的输出应该如下所示:

Ready ... running for 10000ms
instance 1 [x] Received 'Hello.1'
instance 2 [x] Received 'Hello..2'
instance 1 [x] Done in 1.001s
instance 1 [x] Received 'Hello...3'
instance 2 [x] Done in 2.004s
instance 2 [x] Received 'Hello.4'
instance 2 [x] Done in 1.0s
instance 2 [x] Received 'Hello..5'

消息确认

执行任务可能需要几秒钟。你可能会想,如果一个消费者开始一个长期的任务,并且仅仅部分地完成它,就会发生什么。默认情况下,Spring-amqp采用保守的消息确认方式。如果监听器引发异常,容器调用:

channel.basicReject(deliveryTag,requeue)

默认情况下,会重新排队,除非您明确设置:

defaultRequeueRejected = false

或者监听器抛出一个AmqpRejectAndDontRequeueException。这通常是你从接收者那里获得的一种典型行为。在这种模式下,没有必要担心忘记的确认。处理消息后,侦听器调用:

channel.basicAck()

忘记确认


错过basicAck是一个常见的错误,spring-amqp通过默认配置有助于避免这种情况。。这是一个容易的发生的错误,但后果是严重的。当您的客户端退出(可能看起来像随机重新传递)时,消息将被重新传递,但是RabbitMQ将会消耗越来越多的内存,因为它将无法释放任何未包含的消息。

为了调试这种错误,您可以使用rabbitmqctl 打印messages_unacknowledged字段:

sudo rabbitmqctl list_queues name message_ready messages_unacknowledgedged

在Windows上,删除sudo:

rabbitmqctl.bat list_queues name message_ready messages_unacknowledgedged

消息持久化


我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ服务器停止,我们的任务仍然会丢失。
当RabbitMQ退出或崩溃时,它会忘记队列和消息,除非你不告诉它。需要两件事来确保消息不会丢失:我们需要将队列和消息标记为持久。
首先,我们需要确保RabbitMQ不会失去我们的队列。为了这样做,我们需要将其声明为持久的:

boolean durable = true ;
channel.queueDeclare(“hello”,durable,falsefalsenull);

虽然这个命令本身是正确的,但是在我们目前的设置中是不行的。这是因为我们已经定义了一个非持久化的名为hello的队列。RabbitMQ不允许您重新定义具有不同参数的现有队列,并会向尝试执行此操作的任何程序返回错误。但是有一个快速的解决方法 - 让我们用不同的名称声明一个队列,例如task_queue:

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

这个queueDeclare更改需要应用于生产者和消费者代码。

在这一点上,我们确信,即使RabbitMQ重新启动,task_queue队列也不会丢失。现在我们需要通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN来标记我们的消息

注意消息持久性


将消息标记为持久性不能完全保证消息不会丢失。虽然它告诉RabbitMQ将消息保存到磁盘,但是当RabbitMQ接受消息并且尚未保存消息时,仍然有一个很短的时间窗口。此外,RabbitMQ不会对每个消息执行fsync(2) - 它可能只是保存到缓存中,而不是真正写入磁盘。持久性保证不强,但对我们的简单任务队列来说已经足够了。如果您需要更强大的保证,那么您可以使用发布者确认。

公平调度


您可能已经注意到,调度仍然无法正常工作。例如在两个工人的情况下,当所有奇怪的信息都很重,甚至信息很轻的时候,一个工作人员将不断忙碌,另一个工作人员几乎不会做任何工作。那么,RabbitMQ不知道什么,还会平均分配消息。

这是因为当消息进入队列时,RabbitMQ只会分派消息。它不看消费者的未确认消息的数量。它只是盲目地向第n个消费者发送每个第n个消息。

这里写图片描述

为了打破这种方式,我们可以使用basicQos方法与 prefetchCount = 1设置。这告诉RabbitMQ不要一次给一个工作者多个消息。或者换句话说,在处理并确认前一个消息之前,不要向工作进程发送新消息。相反,它将发送到下一个还不忙的工作进程。

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

注意队列大小


如果所有的工人都忙,你的队列可以填满。你会想要注意的是,也许增加更多的工人,或者有其他的策略

完整代码


我们的NewTask.java类的最终代码:

package com.example.rabbitmq;

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

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

/**
 * Author: 王俊超
 * Date: 2017-06-09 08:08
 * All Rights Reserved !!!
 */
public class NewTask {
    private static final String TASK_QUEUE_NAME = "task_queue";

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

        channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

        String message = getMessage(args);

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

        channel.close();
        connection.close();
    }

    private static String getMessage(String[] strings) {
        if (strings.length < 1) {
            return "Hello World!";
        }
        return joinStrings(strings, " ");
    }

    private static String joinStrings(String[] strings, String delimiter) {
        int length = strings.length;
        if (length == 0) {
            return "";
        }
        StringBuilder words = new StringBuilder(strings[0]);
        for (int i = 1; i < length; i++) {
            words.append(delimiter).append(strings[i]);
        }
        return words.toString();
    }
}

和我们的Worker.java:

package com.example.rabbitmq;

import com.rabbitmq.client.*;

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

/**
 * Author: 王俊超
 * Date: 2017-06-09 08:02
 * All Rights Reserved !!!
 */
public class Worker {
    public static final String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

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

        // 指定从消息通道中每次取的消息数量
        channel.basicQos(1);

        final 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(" [x] Received '" + message + "'");
                try {
                    doWork(message);
                } finally {
                    System.out.println(" [x] Done");
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };

        channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
    }

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

使用消息确认和prefetchCount可以设置工作队列。即使RabbitMQ重新启动,耐久性选项也让任务生存下去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值