RabbitMQ工作队列之竞争消费者模式(二)
本篇文章基于之前构建的项目中,详细讲解竞争消费者模式:
- 基于客户端模式使用竞争消费者模式
- 基于spring集成使用竞争消费者模式
- 基于spring boot集成使用竞争消费者模式
RabbitMQ官方竞争消费者模式模型图
竞争消费者模式听起来比较拗口,说白了就是一个生产者,一个队列,多个消费者。
同样是点对点模式,但是在消费者之间,对消费队列是有一些规则策略的,如:公平分发策略,轮询分发策略等等。
目录
用 [TOC]
来生成目录:
1、基于客户端模式使用竞争消费者模式
1.1获取连接工具类
package com.util;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author : alex
* @version :1.0.0
* @Date : create by 2018/7/19 22:01
* @description :获取连接
* @note 注意事项
*/
public class ConnectionUtils {
//获取接连
public static Connection getConnection() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
//设置连接MQ的IP地址
factory.setHost("192.168.153.128");
//设置连接端口号
factory.setPort(5672);
//设置要接连MQ的库(域)
factory.setVirtualHost("/test_vh");
//连接帐号
factory.setUsername("test_mmr");
//连接密码
factory.setPassword("123456");
return factory.newConnection();
}
}
1.2生产者
package com.workqueue;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.util.ConnectionUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author : alex
* @version :1.0.0
* @Date : create by 2018/7/30 21:07
* @description :消息发送者
* @note 注意事项
*/
public class Sender {
//定义队列名称
public static String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//获取连接
Connection connection = ConnectionUtils.getConnection();
//创建channel通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//循环发送30条消息
for (int i = 0; i < 30 ; i++) {
//声明消息
String msg = "this is msg [" + i + "]";
System.out.println("发送消息:"+msg);
//发送消息
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
Thread.sleep(500);
}
//关闭通道
channel.close();
//关闭连接
connection.close();
}
}
1.3消费者1号
package com.workqueue;
import com.rabbitmq.client.*;
import com.util.ConnectionUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author : alex
* @version :1.0.0
* @Date : create by 2018/7/30 21:28
* @description :
* @note 注意事项
*/
public class Customer1 {
public static String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
//获取并转成String
String message = new String(body, "UTF-8");
System.out.println("-->消费者1号,收到消息,msg :"+message);
}
};
//监听队列
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
1.4消费者2号和消费者3号,复制消费者1号,代码都一样(记得把输出的代号改一下)
运行一下查看结果
可以看到按照取模的方式进行分发消息给消费者,每个消费者拿到的消息数量都是一样的。
默认RabbitMQ采用的是轮询分发策略。
1.5接口分析阶段
首先,我们找到找一下RabbitMQ Client的官方文档地址:
版本5.3.0:https://rabbitmq.github.io/rabbitmq-java-client/api/current/overview-summary.html
版本4.7.0:https://rabbitmq.github.io/rabbitmq-java-client/api/4.x.x/overview-summary.html
当前版本为5.3.0
打开到channel接口文档:https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/Channel.html
queueDeclare()接口方法,该方法为声明一个队列
- queue:表示队列的名称
- durable:参数为true表示声明一个持久队列(队列将在服务器重启后继续存在)
- exclusive:参数为true表示声明一个独占队列(仅限于此连接)
- autoDelete:参数为true表示声明一个自动删除队列(服务器将在不再使用时将其删除)
- arguments:队列的其他属性(构造参数)
basicPublish()接口方法,该方法有3个重载,该方法为发布消息
- exchange:将消息发布到的交换器exchange
- routingKey:路由密钥
- mandatory:如果要设置“强制”标志,则为true
- immediate:如果要设置’立即’标志,则为true
- props:消息的其他属性 - 路由头等
- body:消息体
DefaultConsumer类,定义消费者,用于后续的消费
- getChannel():获得使用到的的Channel,定义的搜索频道
- getConsumerTag():获得检索使用者标记。
- handleCancel():取消处理
- handleCancleOk():取消处理ok
- handleConsumeOk():消费处理ok
- handleDelivery():输送处理
- handleRecoverOk():收回ok
- handleShutdownSignal():关闭信号
文档地址:https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/DefaultConsumer.html
消费操作的方法basicConsume(),该方法用于消费消息
该方法重载比较多,就不一一全部讲解了
- queue:队列名称
- autoAck:是否自动确认消息
- callback:之前的定义消费者类
- deliverCallback:交付回调
- cancelCallback:取消回调
- shutdownSignalCallback:关闭信号的回调
- consumerTag:使用者标记
- exclusive:是否独占队列
如果想深入了解,文档地址:https://rabbitmq.github.io/rabbitmq-java-client/api/current/com/rabbitmq/client/Channel.html
1.6、扩展默认的轮询分发机制
首先抛出一个问题,假设消费者分布于不同的服务器上,每个消费者的处理速率不一,如:
消费者1需要执行2秒才结束
消费者2需要执行5秒
消费者3需要执行1秒
那么就会造成服务器的使用率不高,有一些服务器很忙,有一些服务器很闲。我们如何优化呢?
让我们模拟耗时操作:
分别在消费者1,2,3中的定义消费者中,加入以下代码,分别睡眠2、5、1秒
try {
Thread.sleep(2000);//模拟耗时操作,2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
再执行一遍,发现虽然每台服务器执行数量都一样,但是有一些服务器很忙,有一些服务器很闲。
改造成公平分发策略,直接上修改后的代码,再解释:
生产者不需要改动,只需要改动消费者
以消费者1为例:
package com.workqueue;
import com.rabbitmq.client.*;
import com.util.ConnectionUtils;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author : alex
* @version :1.0.0
* @Date : create by 2018/7/30 21:28
* @description :消费者1
* @note 注意事项
*/
public class Customer1 {
public static String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicQos(1);//添加设置,每次处理1个
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
//获取并转成String
String message = new String(body, "UTF-8");
System.out.println("-->消费者1号,收到消息,msg :"+message);
try {
Thread.sleep(2000);//模拟耗时操作,2秒
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
channel.basicAck(envelope.getDeliveryTag(),false);//手动确认
}
}
};
//监听队列
channel.basicConsume(QUEUE_NAME, false, consumer);//这里改为false关闭自动确认
}
}
上述代码改动4处:
- channel.basicQos(1);//添加设置,每次处理1个
- Channel设置为final
- finally 模块添加手动确认:channel.basicAck(envelope.getDeliveryTag(),false);//手动确认
- 最后消费时改为false关闭自动确认
依次修改3个消费者,结束
讲一下思路,在前面轮询分发时,消息是自动确认的,自动确认消息的同时,会一直监听队列获取新任务,哪怕设置当前接收任务数为1,也无法实现。(感兴趣的可以试一下,设置接收任务数为1,开启消息自动确认,移除手动确认)
改造思路:关闭消息自动确认,并且设置当前接收任务处理任务数为1,消费者手动确认消息,在收到确认消息之后,才可以继续监听队列获取新的消息任务。
2、基于spring集成使用竞争消费者模式
2、1轮询分发策略
spring集成后比较简单
在原有的项目基础上(参考RabbitMQ工作队列模式简介,以及简单使用、简单集成spring使用(一) ,地址:https://blog.csdn.net/u011709128/article/details/81263309),添加复制MyCustomer类,粘帖成MyCustomer2和MyCustomer3两个类,print输出带上消费者代号
然后修改rabbitmq.xml文件,定义两个消费者,队列监听添加两个消费者。就可以了
我们运行一下看看
可以看到按照轮询的方式进行分发消息。
2、2改造成公平分发策略
main类
package com.rabbitmq.handle;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author : alex
* @version :1.0.0
* @Date : create by 2018/7/19 23:02
* @description :
* @note 注意事项
*/
public class Main {
public static void main(String[] args) throws InterruptedException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:context.xml");
//获取rabbit模版(等价于@Autowired)
RabbitTemplate bean = context.getBean(RabbitTemplate.class);
//循环发送30条消息
for (int i =0;i<30;i++) {
bean.convertAndSend("test_mmr","hello word"+i);
Thread.sleep(500);//休眠0.5秒
}
Thread.sleep(10000);//休眠2秒后,关闭spring容器
context.close();
}
}
消费者1、消费者2、消费者3,代码其实都一样,但是要把耗时操作改为1秒、3秒、5秒。
实现与ChannelAwareMessageListener后,自定义的监听方法listen()就不起作用了。使用onMessage()消费
package com.rabbitmq.handle;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
/**
* @author : alex
* @version :1.0.0
* @Date : create by 2018/7/19 23:39
* @description :我的消费者
* @note 注意事项
*/
public class MyCustomer implements ChannelAwareMessageListener {
public void listen(String foo){
System.out.println("消费者消费1,获取消息msg:"+foo);
}
public void onMessage(Message message, Channel channel) throws Exception {
try{
//message.getMessageProperties()+
Thread.sleep(1000);//模拟耗时操作1秒
System.out.println("consumer--:"+":"+new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}catch(Exception e){
e.printStackTrace();//TODO 业务处理
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
}
}
更改rabbitmq.xml中队列监听参数,并且定义多个消费者
<!--定义消费者-->
<bean id="myCustomer" class="com.rabbitmq.handle.MyCustomer"/>
<bean id="myCustomer2" class="com.rabbitmq.handle.MyCustomer2"/>
<bean id="myCustomer3" class="com.rabbitmq.handle.MyCustomer3"/>
<!--队列监听 acknowledge应答方式:auto,manual,none prefetch等价于channel.basicQos(1) -->
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual" prefetch="1" >
<rabbit:listener ref="myCustomer" method="listen" queue-names="test_mmr" />
<rabbit:listener ref="myCustomer2" method="listen" queue-names="test_mmr"/>
<rabbit:listener ref="myCustomer3" method="listen" queue-names="test_mmr" />
</rabbit:listener-container>
运行结果:可以看到,消费者1、消费者2、消费者3一开始的时候,就领走了编号0、1、2的消息,但是由于每个消费者中的耗时操作不同,机器的空闲情况不同,后续消费者1消费了:0、3、4之后,消费者2才开始消费第二个消息,而消费者3耗时操作最久,第二次领取消息时,已经是编号9了
3、基于spring boot集成使用竞争消费者模式
3、1默认的多消费者模式最简单。
在原有的项目基础上(参考RabbitMQ工作队列模式简介,以及简单使用、简单集成spring使用(一) ,地址:https://blog.csdn.net/u011709128/article/details/81263309)
基于spring boot实现中,多复制几份CustomerMsg,也就是消费者类出来,就满足了
目录结构图:
运行效果图:
3、2改造成公平分发策略
maven构建spring boot项目
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- rabbitmq -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
主程序入口:DemoApplication
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
配置文件:application.yml
spring:
rabbitmq:
username: root
password: 123456
host: 192.168.199.128
port: 5672
virtual-host: /test_vh
listener:
simple:
acknowledge-mode: manual #设置确认方式
prefetch: 1 #每次处理1条消息
以web的方式访问:IndexController.java
package com.example.demo.controller;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: Alex
* @DateTime: 2018/8/17 16:36
* @Description: 模拟生产者
* @Version: 1.0.0
**/
@RestController
public class IndexController {
@Autowired
private AmqpTemplate amqpTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 使用AmqpTemplate
* @return
* @throws Exception
*/
@PostMapping("/amqpSend")
public String amqpSend() throws Exception{
String msg = "amqp";
for (int i=0;i<20;i++){
amqpTemplate.convertAndSend("test_mmr",msg);//指定发送的队列名称,和数据
System.out.println("序号:"+i+",发送时间:"+System.currentTimeMillis()+",发送消息:"+msg);
Thread.sleep(1000);//1秒
}
return msg;
}
/**
* 使用RabbitTemplate
* @return
* @throws Exception
*/
@PostMapping("/rabbitSend")
public String rabbitSend() throws Exception{
String msg = "rabbit";
for (int i=0;i<20;i++) {
rabbitTemplate.convertAndSend("test_mmr", msg+i);
// System.out.println("生产者,序号:"+i+",发送时间:"+System.currentTimeMillis()+",发送消息:"+msg);
Thread.sleep(500);//0.5秒
}
return msg;
}
}
消费者1
package com.example.demo.rabbitUtil;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author: Alex
* @DateTime: 2018/8/17 16:35
* @Description: 模拟消费者1
* @Version: 1.0.0
**/
@Component
//监听的队列
@RabbitListener(queues = "test_mmr")
public class CustomerMsg {
/**
* 进行接收处理
* @param string
*/
@RabbitHandler
public void onMessage(String string,Channel channel, Message message) throws IOException, InterruptedException {
Thread.sleep(1000);
System.out.println("消费者1,接收时间:"+System.currentTimeMillis()+",收到消息,消息: " + string);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//手动确认
//丢弃这条消息
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
}
消费者2
package com.example.demo.rabbitUtil;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @author: Alex
* @DateTime: 2018/8/17 16:42
* @Description: 模拟消费者2
* @Version: 1.0.0
**/
@Component
//监听的队列
@RabbitListener(queues = "test_mmr")
public class CustomerMsg2 {
/**
* 进行接收处理
* @param string
*/
@RabbitHandler
public void onMessage(String string,Channel channel, Message message) throws InterruptedException, IOException {
Thread.sleep(3000);
System.out.println("消费者2,接收时间:"+System.currentTimeMillis()+",收到消息,消息: " + string);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//手动确认
//丢弃这条消息
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
}
}
项目目录结构图:
运行效果图:
4、小结
真心感叹spring的强大,一次比一次的方便使用
与此同时,rabbitMQ的手动确认消息机制,必须还是channel干活,spring集成rabbitMQ的使用,基于bean方式的注入各种配置,各种设置,如:queue,RabbitListener之类,但是使用起来,和原生的使用的理念不大,只是使用的方式更便捷了。