RabbitMQ(二):工作队列(Work queues)

开发版本为 Spring Boot 2.2.4.RELEASE 版本,开发工具为 Eclipse IDE for Enterprise Java Developers(Version: 2019-09 R (4.13.0)),Jave 版本为 1.8,RabbitMQ 3.8.2,Erlang 22.2。
参考资料
本文为 https://windmt.com/2018/04/12/rabbitmq-2-work-queues/ 的学习笔记。

前言

在前一篇文章中,我们实现了一个简单的发送、接收消息的程序。在本文中,我们将创建一个工作队列,用于在多个消费者之间分发耗时的任务。
RabbitMQ(二):工作队列(Work queues)_第1张图片
工作队列(也称为:任务队列,Task Queues)主要是为了避免等待一些占用大量资源、时间的操作。当我们把任务(Task)当作消息发送到队列中,一个运行在后台的工作者(worker)进程就会取出任务然后处理。当你运行多个工作者(workers),任务就会在它们之间共享。

这个概念在网络应用程序中特别有用,因为在网络应用程序中,不可能在较短的HTTP请求内处理复杂的任务。

准备

在前一篇文章中,我们发送了一条包含 Hello World 的消息。现在我们将发送一些字符串来当作复杂任务。我们并没有一个真实的复杂任务,类似于图片大小被调整或 pdf 文件被渲染,所以我们通过 sleep () 方法来模拟这种情况。我们在字符串中加上点号(.)来表示任务的复杂程度,一个点(.)将会耗时 1 秒钟。比如 “Hello…” 就会耗时 3 秒钟。

代码实现

  1. 配置类
    tut1 类似,首先创建包 tut2 来存放本文的相关代码。
    新增配置类 Tut2Config:
package com.example.demo.tut2;

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

@Profile({ "tut2", "work-queues" })
@Configuration
public class Tut2Config {
	@Bean
	public Queue hello() {
		return new Queue("work-queues");
	}

	@Profile("receiver")
	private static class ReceiverConfig {

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

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

	}

	@Profile("sender")
	@Bean
	public Tut2Sender sender() {
		return new Tut2Sender();
	}
}

其中,我们添加了两个配置文件:tut2work-queues。我们利用 Spring 将队列公开为 bean,并在配置消费者时,定义两个 bean,对应了上图中的两个消费者 C1 和 C2

  1. 生产者
    简单修改下生产者的代码,在消息 hello 后自动添加 .
package com.example.demo.tut2;

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;

import java.util.concurrent.atomic.AtomicInteger;

public class Tut2Sender {
	@Autowired
	private RabbitTemplate template;

	@Autowired
	private Queue queue;
	
	AtomicInteger dots = new AtomicInteger(0);

	AtomicInteger count = new AtomicInteger(0);
	
	/**
	 * 用定时任务来模拟生产者定时发送消息
	 */
	@Scheduled(fixedDelay = 1000, initialDelay = 500)
	public void send() {
		StringBuilder builder = new StringBuilder("Hello");
		
		if (dots.getAndIncrement() == 3) {
			dots.set(1);
		}
		
		for (int i = 0; i < dots.get(); i++) {
			builder.append('.');
		}
		
		builder.append(count.incrementAndGet());
		
		String message = builder.toString();
		this.template.convertAndSend(queue.getName(), message);
		System.out.println(" [x] Sent '" + message + "'");
	}
}
  1. 消费者
    Tut2Receiver 中,我们通过 doWork() 方法模拟了一个耗时的虚假任务,每有一个 .,就多花 1 秒的操作。为 Tut2Receiver 添加了一个实例编号,用以显示是哪个实例消费了消息和处理的时长。
package com.example.demo.tut2;

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

@RabbitListener(queues = "work-queues")
public class Tut2Receiver {

	private int instance;

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

	@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);
		watch.stop();
		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);
			}
		}
	}
}

运行

  1. 右键项目,依次执行 Run As -> Maven clean 和 Run As -> Maven install,生成 jar 包。
  2. 打开 cmd,切换到工程目录,如 D:\eclipse-workspace\rabbitmq-tutorial,分别执行以下语句:
#shell1 消费者
java -jar target/rabbitmq-tutorial-0.0.1-SNAPSHOT.jar --spring.profiles.active=tut2,receiver  --tutorial.client.duration=60000
#shell2 生产者
java -jar target/rabbitmq-tutorial-0.0.1-SNAPSHOT.jar --spring.profiles.active=tut2,sender  --tutorial.client.duration=60000
  1. 消费者的消息如下:
// 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'

// Receiver
Ready ... running for 10000ms
instance 1 [x] Received 'Hello.1'
instance 2 [x] Received 'Hello..2'
instance 1 [x] Done in 1.005s
instance 1 [x] Received 'Hello...3'
instance 2 [x] Done in 2.007s
instance 2 [x] Received 'Hello.4'
instance 2 [x] Done in 1.005s
instance 1 [x] Done in 3.01s
instance 1 [x] Received 'Hello..5'
instance 2 [x] Received 'Hello...6'
instance 1 [x] Done in 2.006s
instance 1 [x] Received 'Hello.7'
instance 1 [x] Done in 1.002s
instance 1 [x] Received 'Hello...9'
instance 2 [x] Done in 3.01s
instance 2 [x] Received 'Hello..8'

可以看到 C1 和 C2 是依次接收到消息的。我认为 在 Spring 中,RabbitMQ 分发的消息逻辑应是循环调度:判断 C1 的未确认消息数是否小于 prefetchCount,若小于,则将消息分派给 C1,否则判断下一个消费者 C2 是否符合,依次循环往复,而 Spring 中,prefetchCount 的默认值为 250(于org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer 中 看到 DEFAULT_PREFETCH_COUNT 设置为了250)。

修改 prefetch

  1. 方法1
    可在 application.yml 中设置 spring.rabbitmq.listener.simple.prefetch=1,这会影响到本 Spring Boot 应用中所有使用默认 SimpleRabbitListenerContainerFactory 的消费者。
  2. 方法2
    为了只针对特定的消费者,如本文中,只针对 Tut2Receiver2,那么可以通过在 Tut2Config 配置类中添加以下 Bean:
@Bean
public RabbitListenerContainerFactory<SimpleMessageListenerContainer> prefetchOneRabbitListenerContainerFactory(ConnectionFactory rabbitConnectionFactory) {
	SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
	factory.setConnectionFactory(rabbitConnectionFactory);
	factory.setPrefetchCount(1);
	return factory;
}

并在 Tut2Receiver2 的RabbitListener 注解中指定 containerFactory

@RabbitListener(queues = "work-queues", containerFactory = "prefetchOneRabbitListenerContainerFactory")

若将 prefetch 设置为 1,则将得到以下结果:

Ready ... running for 60000ms
instance 1 [x] Received 'Hello.1'
instance 2 [x] Received 'Hello..2'
instance 1 [x] Received 'Hello...3'
instance 2 [x] Received 'Hello.4'
instance 2 [x] Received 'Hello..5'
instance 1 [x] Received 'Hello...6'
instance 2 [x] Received 'Hello.7'
instance 2 [x] Received 'Hello..8'
instance 1 [x] Received 'Hello...9'
instance 2 [x] Received 'Hello.10'
instance 2 [x] Received 'Hello..11'
instance 1 [x] Received 'Hello...12'

此结果可通过以上循环调度的逻辑解释。

你可能感兴趣的:(RabbitMQ,Spring)