1 问题引入
在实际环境下,每个消息的“大小”是不同的,所需要的处理时间也是不同的。在这种情况下,我们应该如何分配资源来令效率最大化呢?这就需要我们来学习 RabbitMQ 的消息分发机制了。
2 回顾分发机制
以下分发机制的内容来自我的前几篇博客,这里借用一下。
一般来说,一个队列有多个消费者同时消费数据,此时有两种分发数据的方式,一种是轮询分发,另一种是公平分发。
2.1 轮询分发
轮询分发,Round-robin dispatching,也是 RabbitMQ 的默认消息分发机制。队列会给每一个消费者发送的数据数量是一模一样的,并不会因为两个消费者处理数据速度不同而发生改变。这样很容易导致部分消费者天天加班,而部分消费者无所事事的情况。
轮询分发是 RabbitMQ 默认的消息分发机制。
2.2 公平分发
公平分发,Fair dispatch,消费者设置每次从队列里只取一条数据,并且关闭自动回复机制,每次取完一条数据后,手动回复并继续取下一条数据。这样每个消费者都只有消费完一条消息才会收到下一条消息,十分的“公平”。(每个消费者在同一时间点最多处理的消息个数由 prefetchCount 参数规定,而不是固定为1个)
需要注意的是,如果我们使用公平分发,那么必须关闭自动应答,同时改为手动应答。
3 测试
在 Spring Boot 环境下,RabbitMQ 默认使用何种分发机制呢?下面我们做一个小小的测试。
3.1 测试1
3.1.1 生产者设置
设置生产者配置类
package com.example.provider.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 直连型交换机
* @author 30309
*
*/
@Configuration
public class DirectRabbitConfig {
//队列,名称为DirectQueue
@Bean
public Queue DirectQueue() {
return new Queue("DirectQueue",true); //true表示是否持久
}
//直连型交换机,名称为DirectExchange
@Bean
DirectExchange DirectExchange() {
return new DirectExchange("DirectExchange");
}
//将队列和交换机绑定, 并设置用于匹配键:DirectRouting
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(DirectQueue()).to(DirectExchange()).with("DirectRouting");
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 消息发送失败返回到队列中, 配置文件需要配置 publisher-returns: true
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
}
设置生产者控制器
package com.example.provider.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 生产者
* @author 30309
*
*/
@RestController
public class SendMessageController{
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendDirectMessage")
public String sendDirectMessage() {
for(int i = 0;i < 10;i++) {
rabbitTemplate.convertAndSend("DirectExchange", "DirectRouting", (long)1000);
}
return "ok";
}
}
生产者配置
server.port: 8080
spring.application.name: provider
spring.rabbitmq.host: 127.0.0.1
spring.rabbitmq.port: 5672
spring.rabbitmq.username: guest
spring.rabbitmq.password: guest
spring.rabbitmq.virtual-host: /
# 开启 confirm 确认机制
spring.rabbitmq.publisher-confirms: true
# 开启 return 确认机制
spring.rabbitmq.publisher-returns: true
# 手动应答
spring.rabbitmq.listener.simple.acknowledge-mode: manual
# 指定最小的消费者数量
spring.rabbitmq.listener.simple.concurrency: 1
# 指定最大的消费者数量
spring.rabbitmq.listener.simple.max-concurrency: 2
# 是否支持重试
spring.rabbitmq.listener.simple.retry.enabled: true
3.1.2 消费者设置
设置消费者配置类
package com.example.consumer.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 直连型交换机
* @author 30309
*
*/
@Configuration
public class DirectRabbitConfig {
//队列,名称为DirectQueue
@Bean
public Queue DirectQueue() {
return new Queue("DirectQueue",true); //true表示是否持久
}
//直连型交换机,名称为DirectExchange
@Bean
DirectExchange DirectExchange() {
return new DirectExchange("DirectExchange");
}
//将队列和交换机绑定, 并设置用于匹配键:DirectRouting
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(DirectQueue()).to(DirectExchange()).with("DirectRouting");
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 消息发送失败返回到队列中, 配置文件需要配置 publisher-returns: true
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
}
消费者1
package com.example.consumer.receiver;
import java.io.IOException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
/**
* 消费者1
* @author 30309
*
*/
@Component
public class DirectReceiver1 {
@RabbitListener(queues = "DirectQueue")//监听的队列名称为DirectQueue
@RabbitHandler
public void process(long i,Channel channel, Message message) {
System.out.println("DirectReceiver1消费者收到消息:需要延时" + i);
work(i);
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
e.printStackTrace();
}
}
//延时,模拟消息的处理
private void work(long timeout) {
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者2
package com.example.consumer.receiver;
import java.io.IOException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
/**
* 消费者2
* @author 30309
*
*/
@Component
public class DirectReceiver2 {
@RabbitListener(queues = "DirectQueue")//监听的队列名称为DirectQueue
@RabbitHandler
public void process(long i,Channel channel, Message message) {
System.out.println("DirectReceiver2消费者收到消息:需要延时" + i);
work(i);
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
e.printStackTrace();
}
}
//延时,模拟消息的处理
private void work(long timeout) {
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者配置
server.port: 8079
spring.application.name: consumer
spring.rabbitmq.host: 127.0.0.1
spring.rabbitmq.port: 5672
spring.rabbitmq.username: guest
spring.rabbitmq.password: guest
spring.rabbitmq.virtual-host: /
# 开启 confirm 确认机制
spring.rabbitmq.publisher-confirms: true
# 开启 return 确认机制
spring.rabbitmq.publisher-returns: true
# 手动应答
spring.rabbitmq.listener.simple.acknowledge-mode: manual
# 指定最小的消费者数量
spring.rabbitmq.listener.simple.concurrency: 1
# 指定最大的消费者数量
spring.rabbitmq.listener.simple.max-concurrency: 2
# 是否支持重试
spring.rabbitmq.listener.simple.retry.enabled: true
3.1.3 测试结果
结果如下
我们可以发现,消费者1与消费者2各自处理了5条消息。在两个消费者处理能力几乎一样的情况下,两个消费者处理消息几乎是同步的。这样其实我们并不能看出点什么,下面我们修改一下代码,让两个消费者的处理能力变为不同,看看会发生些什么。
3.2 测试2
3.2.1 修改生产者
package com.example.provider.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 生产者
* @author 30309
*
*/
@RestController
public class SendMessageController{
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendDirectMessage")
public String sendDirectMessage() {
rabbitTemplate.convertAndSend("DirectExchange", "DirectRouting", (long)5000);
for(int i = 0;i < 5;i++) {
rabbitTemplate.convertAndSend("DirectExchange", "DirectRouting", (long)1000);
}
return "ok";
}
}
这次我们生产了6条消息,其中一条消息的处理时间为5秒,另外五条消息的处理时间为1秒。结果如下:
我们可以看到,每个消费者各自处理了三条消息,可发现在默认情况下是按轮询分发的模式来分发消息的。
4 公平分发模式的使用
修改一下配置文件,增加一行配置
# 指定一个请求能处理多少消息
spring.rabbitmq.listener.simple.prefetch: 1
再次运行,结果如下
我们发现,修改配置以后,我们仍然生产了6条消息,其中一条消息的处理时间为5秒,另外五条消息的处理时间为1秒。与上次不同的是,消费者1只处理了一条消息,消费者2处理了5条消息,可见现在已使用公平分发的模式来分发消息。