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 测试结果

结果如下

rabbitmq 生产消息监控_数据


我们可以发现,消费者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秒。结果如下:

rabbitmq 生产消息监控_分发机制_02


我们可以看到,每个消费者各自处理了三条消息,可发现在默认情况下是按轮询分发的模式来分发消息的。

4 公平分发模式的使用

修改一下配置文件,增加一行配置

# 指定一个请求能处理多少消息
spring.rabbitmq.listener.simple.prefetch: 1

再次运行,结果如下

rabbitmq 生产消息监控_RabbitMQ_03


我们发现,修改配置以后,我们仍然生产了6条消息,其中一条消息的处理时间为5秒,另外五条消息的处理时间为1秒。与上次不同的是,消费者1只处理了一条消息,消费者2处理了5条消息,可见现在已使用公平分发的模式来分发消息。