RabbitMQ 工作队列
第六种RPC模式还没研究 ,表面上是五种模式,其实只有三种,一种是简单队列,二是工作队列,三是和交换器绑定的合起的
1.1 简单队列(模式)
p :producer 生产者
hello :队列名称
c:consumer 消费者
1.1.1 简单模式介绍
将消息发送给唯一一个的节点时就使用这种模式,这是最简单的形式
简单理解就是,生产者发送消息到队列,消费者从队列种取出消息
该模式特点
- 1,一般情况下使用rabbitMQ自带的Exchange:“”
- 2,这种模式下不需要Exchange进行任何绑定(binding)操作
- 3,消息传递时需要一个RouteKey,可以简单的理解为要发送到的队列的名字
- 4,如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃
1.1.2 创建队列
Virtual host: 虚拟主机(自带 /) Durability:是否做持久化 Durable(持久) transient(临时)Auto delete : 是否自动删除
1.1.3 代码实现
1,先引入依赖,mavne项目
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.4.3</version>
</dependency>
2,创建一个连接工具类
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @Author : FuYu
* @Despriotion : rabbitmq连接工具类
*/
public class ConnectionUtil {
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口 程序用5672 网页用 15672
factory.setPort(5672);
//虚拟主机 没创建就用 /
factory.setVirtualHost("/");
//用户名
factory.setUsername("guest");
//密码
factory.setPassword("guest");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
3,消息生产者 send.java
import com.fy.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* @Author : FuYu
* @Despriotion : 消息生产者
*/
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
try (Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel()) {
// 声明(创建)队列,如果虚拟主机种没有改队列名称,会创建一个
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//发送的消息
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
控制台打印
[x] Sent 'Hello World!'
看rabbitMQ界面
点击hello,进入查看消息
4,消息消费者rev.java
import com.fy.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DeliverCallback;
/**
* @Author : FuYu
* @Despriotion :消息消费者
*/
public class Recv {
//队列名称
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//服务器将队列中的消息传递给我们。由于它将异步地向我们发送消息,
// 因此我们以对象的形式提供了一个回调,该回调将缓冲消息,直到我们准备使用它们为止
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
//deliverCallback 回调 打印
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
控制台打印
[x] Received 'Hello World!'
消费掉了,队列种没有了
开启多个消费者,每次也只有一个能消费,还不确定是哪个消费者消费,就会有这种情况 有的消费多,有的消费少,就可能形成了累的累死,闲的闲死,下面介绍work模式
1.2 工作模式
一个生产者,多个消费者
1.2.1 工作模式介绍
和简单模式类似,不过比简单模式多了公平派遣控制
1.1.2 创建队列
创建一个work队列,和简单模式创建一样
1.1.3 代码实现
1,消息新生产者类NewTask.java
import com.fy.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
/**
* @Author : FuYu
* @Despriotion : 消息生产者
*/
public class NewTask {
//队列名称
private static final String TASK_QUEUE_NAME = "work";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
try (Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel()) {
//队列设置为持久,但是如果之前存在一个task_queue 队列不是持久的,会出错,必须重新设置一个新的队列
boolean durable = true;
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);
//为了方便控制台输入
String message = String.join(" ", argv);
//将消息标记为持久性-通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN。
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
2,消息消费类Worker.java
import com.fy.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DeliverCallback;
/**
* @Author : FuYu
* @Despriotion :
*/
public class Worker {
private static final String TASK_QUEUE_NAME = "work";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
final Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
final Channel channel = connection.createChannel();
//队列设置为持久,但是如果之前存在一个task_queue 队列不是持久的,会出错,必须重新设置一个新的队列
boolean durable = true;
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 公平派遣 同一时刻服务器只会发一条消息给消费者
//这告诉RabbitMQ一次不要给工人一个以上的消息。换句话说,在处理并确认上一条消息之前,不要将新消息发送给工作人员。而是将其分派给不忙的下一个工作程序。
channel.basicQos(1);
//它需要为消息正文中的每个点伪造一秒钟的工作。它将处理传递的消息并执行任务
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
//开启这行 表示使用手动确认模式
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
//boolean autoAck = false;
//监听队列,false表示手动返回完成状态,true表示自动
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
//模拟执行时间的假任务:
private static void doWork(String task) {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
3,消息确认
默认情况下,RabbitMQ将每个消息依次发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。与三个或更多的工人一起尝试。
如果执行任务需要几秒,而某个消费者在漫长处理过程中死亡,那么消息就会丢失,所以为了确保消息不丢失,一个消费者还没处理完死亡,会把消息交给其他消费者处理,而rabbitMQ有 消息确认 消费者发送回一个确认(告知),告知RabbitMQ特定的消息已被接收,处理,并且RabbitMQ可以自由删除它。
如果使用者死了(其通道已关闭,连接已关闭或TCP连接丢失)而没有发送确认,RabbitMQ将了解消息未完全处理,并将重新排队。如果同时有其他消费者在线,它将很快将其重新分发给另一个消费者。这样,您可以确保即使工人偶尔死亡也不会丢失任何消息。
没有任何消息超时;消费者死亡时,RabbitMQ将重新传递消息。即使处理一条消息花费非常非常长的时间也没关系。
默认情况下,手动消息确认处于打开状态。在前面的示例中,我们通过 autoAck = true 标志显式关闭了它们。现在,是时候将该标志设置为false,并在完成任务后从工作人员发送适当的确认。
//boolean autoAck = false;
//监听队列,false表示手动返回完成状态,true表示自动
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
4,消息持久
任务已经不会因为消费者死亡而丢失任务了,但是,如果RabbitMQ服务器停止,我们的任务仍然会丢失。
RabbitMQ退出或崩溃时,它将忘记队列和消息,除非您告知不要这样做。要确保消息不会丢失,需要做两件事:我们需要将 队列 和 消息 都标记为持久。
//队列设置为持久,但是如果之前存在一个task_queue 队列不是持久的,会出错,必须重新设置一个新的队列
boolean durable = true;
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);
//将消息标记为持久性-通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN。
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
1.3 发布/订阅模式
从这个模式开始都是和交换机打交道
X:Exchange 交换机
RabbitMQ消息传递模型中的核心思想是生产者从不将任何消息直接发送到队列。实际上,生产者经常甚至根本不知道是否将消息传递到任何队列。相反,生产者只能将消息发送到交换机。交流是一件非常简单的事情。一方面,它接收来自生产者的消息,另一方面,将它们推入队列。
交换类型:交易所必须确切知道如何处理收到的消息。是否应将其附加到特定队列?是否应该将其附加到许多队列中?还是应该丢弃它。规则由交换类型定义
交换类型:direct,topic,headers 和fanout
1.3.1 分裂模式
fanout 又叫分裂,当我们需要将消息一次发给多个队列时,需要使用这种模式
1.可以理解为路由表的模式
2.这种模式不需要RouteKey
3.这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个
Queue,一个Queue可以同多个Exchange进行绑定。
4.如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。
1.3.2 交换器绑定队列
1.3.2.1 自动生成队列绑定
在Java客户端中,当我们不向queueDeclare()提供任何参数时,我们将 使用生成的名称创建一个非持久的,排他的,自动删除的随机名称队列:
String queueName = channel.queueDeclare().getQueue();
//交换和队列之间的关系称为绑定
//如果没有队列绑定到交换,消息将丢失。如果没有消费者在听,我们可以安全地丢弃该消息
channel.queueBind(queueName, EXCHANGE_NAME, "");
1.3.2.2 手动绑定
代码绑定,前面有,这里演示通过web绑定
1.3.3 代码实现
1,消息生产者
import com.fy.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* @Author : FuYu
* @Despriotion : 消息生产者
*
*/
public class EmitLog {
//交换机名称
private static final String EXCHANGE_NAME = "fanout";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
try ( Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel()) {
//交换类型 fanout 分裂 它只是将接收到的所有消息广播到它知道的所有队列中
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = argv.length < 1 ? "info: Hello World2!" : String.join(" ", argv);
//第一个参数是交换机的名称。空字符串表示默认或无名称交换:消息将以routingKey指定的名称路由到队列(如果存在)
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
2,消息消费者
import com.fy.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DeliverCallback;
/**
* @Author : FuYu
* @Despriotion : 第二个程序将接收并打印它们。
* 接收器程序的每个运行副本都将获得消息 分裂模式 Fanout
*/
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "fanout";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//在Java客户端中,当我们不向queueDeclare()提供任何参数时,我们将 使用生成的名称创建一个非持久的,排他的,自动删除的队列:
String queueName = channel.queueDeclare().getQueue();
//交换和队列之间的关系称为绑定
//如果没有队列绑定到交换,消息将丢失。如果没有消费者在听,我们可以安全地丢弃该消息
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
运行:开启一个生产者类,开启多个消费者类,会发现每个消费者都接受相同消息(这里用的是自动生成绑定的队列所以开启的消费者都会有打印,演示没有绑定会不会打印 就设置没有被绑定的的队列名称)
// 从连接中创建通道
Channel channel = connection.createChannel();
channel.queueDeclare(“test”, false, false, false, null);
这个模式就是谁绑定了交换机,谁就可以消费消息,没绑定的就没有份,就像看抖音视频,你关注了这个作者,他更新你就会收到消息,没有关注就没有消息推送
1.4 路由模式
1.4.1 路由模式介绍
发布订阅模式绑定的是:交换和队列之间的关系
channel.queueBind(queueName,EXCHANGE_NAME,“”);
路由模式还额外绑定的了:绑定可以采用额外的routingKey参数。为了避免与basic_publish参数混淆,我们将其称为 binding key ,binding key 的含义取决于交换类型,像之前的fanout 就对这个没有价值
channel.queueBind(queueName,EXCHANGE_NAME,“ black”);
用相同的binding key 绑定多个队列是完全合法的
简单理解路由模式
: 分发消息和交换类型有关,放消息到队列中去和binding key 有关,假设,Q1绑定了交换机X1 ,交换类型为drict,那么它分发消息就和简单队列一样,Q1还绑定了 routing key(binding key) 为 black 那么他就只能接受routing key 为black 的值 (ps,等下topic 模式出来就理解了)
1.4.2 创建队列与绑定
(1)新建一个交换器 ,类型选择fanout
(2)点击新建的交换器,绑定队列和匹配规则
1.4.3 代码实现
1,消息生产者类EmitLogDirect.java
import com.fy.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* @Author : FuYu
* @Despriotion : 消息生产者
* argv = routing key 修改 argv 来传不同的值,
* 如 black.1 ,black.2 ,red.1 ,red.2
* 如果queue绑定的binding key 为 black.# 则就不会接受red.#,每个没有一个人介绍red.#,那么red.1 或red.2 会丢弃
*/
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
try (Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String severity = getSeverity(argv);
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
}
}
private static String getSeverity(String[] strings) {
if (strings.length < 1)
return "info";
return strings[0];
}
private static String getMessage(String[] strings) {
if (strings.length < 2)
return "Hello World!";
return joinStrings(strings, " ", 1);
}
private static String joinStrings(String[] strings, String delimiter, int startIndex) {
int length = strings.length;
if (length == 0) return "";
if (length <= startIndex) return "";
StringBuilder words = new StringBuilder(strings[startIndex]);
for (int i = startIndex + 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}
2,消息消费者类ReceiveLogsDirect.java
import com.fy.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*;
/**
* @Author : FuYu
* @Despriotion : 消息消费者
* argv = binding key 绑定binding key 接受什么样的值
* 符号# 匹配一个或多个词,符号* 匹配不多不少一个词
* 如 black.# 可以接受 black.1 ,black.2 , balck.2.ss ,black.xxx.xxx
* 但是balck.* 只能接受 black.1 ,black.2 black.xxx
*/
public class ReceiveLogsDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = channel.queueDeclare().getQueue();
if (argv.length < 1) {
System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");
System.exit(1);
}
//绑定是交换和队列之间的关系--只接受argv[]中的值,多得被丢弃
for (String severity : argv) {
//绑定可以采用额外的routingkey参数,为了避免与basic_publish参数混淆,我们将其称为binding key
//binding key的含义取决于交换类型 direct就不管binding key
channel.queueBind(queueName, EXCHANGE_NAME, severity);
}
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
3,idea运行
消息生产者产生routing key为info 的消息
消息消费者1
消息消费者2
运行info生产者,消费者1打印信息,运行error生产者,消费者2打印信息,同理balck对应消费者2,red对应消费者1
1.5 主题模式
网上的图
1.5.1 topic模式介绍
任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上
如上图所示
此类交换器使得来自不同的源头的消息可以到达一个对列,其实说的更明白一点就是模糊匹配的意思,例如:上图中红色对列的routekey为usa.#,#代表匹配任意字符,但是要想消息能到达此对列,usa.必须匹配后面的#好可以随意。图中usa.newsusa.weather,都能找到红色队列,符号# 匹配一个或多个词,符号* 匹配不多不少一个词。因此usa.# 能够匹配到usa.news.XXX ,但是usa.* 只会匹配到usa.XXX 。
注:交换器说到底是一个名称与队列绑定的列表。当消息发布到交换器时,实际上是由你所连接的信道,将消息路由键同交换器上绑定的列表进行比较,最后路由消息。任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上
该模式特点和路由模式类似,稍微复杂些
- 1,这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能RouteKey模糊匹配的队列。
- 2,这种模式需要RouteKey,也许要提前绑定Exchange与Queue
- 3,在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及
- 4.“#”表示0个或若干个关键字,“”表示一个关键字。如“log.”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。log的消息(一个RouteKey为”MQ.log.error”的消息会被转发到该队列)。
- 5,同样,如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息
1.5.2 创建队列与绑定
和路由模式一样,只是交换类型和匹配规则选择不一样
(1)新建一个交换器 ,类型选择topic
(2)点击新建的交换器topictest,绑定队列和匹配规则
匹配规则这里用XXX.# 或XXX.*
1.5.3 代码实现
代码和路由模式类似
1,消息生产者
/**
* @Author : FuYu
* @Despriotion : 消息生产者
*/
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
try (Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String routingKey = getRouting(argv);
String message = getMessage(argv);
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
}
}
private static String getRouting(String[] strings) {
if (strings.length < 1)
return "anonymous.info";
return strings[0];
}
private static String getMessage(String[] strings) {
if (strings.length < 2)
return "Hello World!";
return joinStrings(strings, " ", 1);
}
private static String joinStrings(String[] strings, String delimiter, int startIndex) {
int length = strings.length;
if (length == 0) return "";
if (length < startIndex) return "";
StringBuilder words = new StringBuilder(strings[startIndex]);
for (int i = startIndex + 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}
2,消息消费者
/**
* @Author : FuYu
* @Despriotion : 消息消费者
*/
public class ReceiveLogsTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String queueName = channel.queueDeclare().getQueue();
if (argv.length < 1) {
System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
System.exit(1);
}
for (String bindingKey : argv) {
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
演示:略
1.6 记录一个注意点
如果虚拟主机上有一个交换机叫name1 这时候用代码也操作的name1,要设置队列要设置成持久的不然会保持
心得:学习还是得记笔记,之前好多东西都没做笔记,都是简单得记一下,好记性不如烂笔头啊,加油了,靓仔,以后要坚持写博客了,有点晚了,哈哈,写着写着就忘我了
翻了很多博客教程还是有很多收获得,代码是官网上的,本人都运行过一遍了,应该没啥问题!