开发版本为 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/ 的学习笔记。
在前一篇文章中,我们实现了一个简单的发送、接收消息的程序。在本文中,我们将创建一个工作队列,用于在多个消费者之间分发耗时的任务。
工作队列(也称为:任务队列,Task Queues)主要是为了避免等待一些占用大量资源、时间的操作。当我们把任务(Task)当作消息发送到队列中,一个运行在后台的工作者(worker)进程就会取出任务然后处理。当你运行多个工作者(workers),任务就会在它们之间共享。
这个概念在网络应用程序中特别有用,因为在网络应用程序中,不可能在较短的HTTP请求内处理复杂的任务。
在前一篇文章中,我们发送了一条包含 Hello World
的消息。现在我们将发送一些字符串来当作复杂任务。我们并没有一个真实的复杂任务,类似于图片大小被调整或 pdf 文件被渲染,所以我们通过 sleep () 方法来模拟这种情况。我们在字符串中加上点号(.)来表示任务的复杂程度,一个点(.)将会耗时 1 秒钟。比如 “Hello…” 就会耗时 3 秒钟。
tut1
类似,首先创建包 tut2
来存放本文的相关代码。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();
}
}
其中,我们添加了两个配置文件:tut2
和 work-queues
。我们利用 Spring 将队列公开为 bean,并在配置消费者时,定义两个 bean,对应了上图中的两个消费者 C1 和 C2。
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 + "'");
}
}
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);
}
}
}
}
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
// 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)。
application.yml
中设置 spring.rabbitmq.listener.simple.prefetch=1
,这会影响到本 Spring Boot 应用中所有使用默认 SimpleRabbitListenerContainerFactory
的消费者。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'
此结果可通过以上循环调度的逻辑解释。