消息队列
MQ消息队列
一. 消息队列的简介
1.简介
在计算机科学中,消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的数据,包含发生的时间,输入设
备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列互交。消息会保存在队列中,直到接收者取回它。消息队列常常保存在链表结构中。拥有权限的进程可以向消息队列中写入或读取消息。
目前,有很多消息队列有很多开源的实现,包括 JBoss Messaging、JORAM、Apache ActiveMQ、Sun Open Message Queue、IBM MQ、Apache Qpid 和 HTTPSQS。
当前使用较多的消息队列有 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq等,而部分数据库如 Redis、Mysql 以及 phxsql 也可实现消息队列的功能。
- MQ 是消费者-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取或者订阅队列中的消息。MQ 和 JMS 类似,但不同的是 JMS 是 SUN JAVA 消息中间件服务的一个标准和 API 定义,而 MQ 则是遵循了 AMQP 协议的具体实现和产品。
注意:
1. AMQP,即 Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。
2. JMS,Java 消息服务(Java Message Service)应用程序接口,是一个 Java 平台中关于面向消息中间件的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。 Java 消息服务是一个与具体平台无关的 API,绝大多数 MOM 提供商都对 JMS
提供支持。常见的消息队列,大部分都实现了 JMS API,如 ActiveMQ,Redis 以及 RabbitMQ等.
- 优缺点
3.1. 优点:应用耦合、异步处理、流量削锋
3.2. 缺点:系统可用性降低、系统复杂性增加
二. 安装消息队列
- 安装Erlang
1.1 根据电脑下载32位或者64位操作系统进行安装
1.2 配置环境变量
1.3 测试是否成功:检查环境变量是否配置成功,cmd 输入 erl ,若出现版本号表明安装成功
- 安装 RabbitMQ
2.1 下载文件:http://www.rabbitmq.com/download.html
2.2 安装并运行 rabbitmq-server-3.7.12.exe
2.3 测试:打开命令行,进入 RabbitMQ 的安装目录:
cd D:\RabbitMQ Server\rabbitmq_server-3.7.12\sbin,
输入 rabbitmqctl.bat status,如果出现以下的图,说明安装是成功的.现在 RabbitMQ Server 已经启动了,运行正常。否则去管理查看 RabbitMQ 服务是否开启。
2.4 安装插件:
输入:rabbitmq-plugins.bat enable rabbitmq_management(安装 UI 插件)
2.5 访问:浏览器输入:127.0.0.1:15672
2.6 添加用户
2.7 分配权限
三. 测试项目中应用消息队列
- 创建maven项目
1.1. 添加依赖
<!-- rabbitmq 依赖 -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.6.0</version>
</dependency>
1.2 创建send类
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 简单模式队列-消息发送者
*/
public class Send {
// 队列名称
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
Connection connection = null;
Channel channel = null;
try {
// 通过工厂创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
/**
* 声明队列
* 第一个参数 queue:队列名称
* 第二个参数 durable:是否持久化
* 第三个参数 Exclusive:排他队列,如果一个队列被声明为排他队列,该队列仅对
首次声明它的连接可见,并在连接断开时自动删除。
* 这里需要注意三点:
* 1. 排他队列是基于连接可见的,同一连接的不同通道是可以同时访问同一
个连接创建的排他队列的。
* 2. "首次",如果一个连接已经声明了一个排他队列,其他连接是不允许建
立同名的排他队列的,这个与普通队列不同。
* 3. 即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都
会被自动删除的。
* 这种队列适用于只限于一个客户端发送读取消息的应用场景。
* 第四个参数 Auto-delete:自动删除,如果该队列没有任何订阅的消费者的话,该
队列会被自动删除。
* 这种队列适用于临时队列。
*/
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 + "'");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 关闭通道
if (null != channel && channel.isOpen())
channel.close();
// 关闭连接
if (null != connection && connection.isOpen())
connection.close();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.3创建rev类
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 简单模式队列-消息接收者
*/
public class Recv {
// 队列名称
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
Connection connection = factory.newConnection();
// 获取通道
Channel channel = connection.createChannel();
// 指定队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// ---------------------之前旧版本的写法-------begin-----------
/*
// 获取消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 获取消息并在控制台打印
String message = new String(body, "utf-8");
System.out.println(" [x] Received '" + message + "'");
}
};
// 监听队列
channel.basicConsume(QUEUE_NAME, true, consumer);
*/
// ---------------------之前旧版本的写法--------end------------
// 获取消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
// 监听队列
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
1.4 运行send类,再运行rev类
四. RabbitMQ 基本概念介绍
1.对象介绍:ConnectionFactory、Connection、Channel
ConnectionFactory、Connection、Channel 都是 RabbitMQ 对外提供的 API 中最基本的对象。Connection 是 RabbitMQ 的 socket 连接,它封装了 socket 协议相关部分逻辑。ConnectionFactory 为 Connection 的制造工厂。Channel 是我们与 RabbitMQ 打交道的最重要的一个接口,我们大部分的业务操作是在Channel 这个接口中完成的,包括定义 Queue、定义 Exchange、绑定 Queue 与 Exchange、
发布消息等.
- Queue Queue(队列)是 RabbitMQ 的内部对象,用于存储消息.
- Message acknowledgment在实际应用中,可能会发生消费者收到 Queue 中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们
可以要求消费者在消费完消息后发送一个回执给 RabbitMQ,RabbitMQ 收到消息回执(Message acknowledgment)后才将该消息从 Queue 中移除;如果 RabbitMQ 没有收到回执并检测到消费者的 RabbitMQ 连接断开,则 RabbitMQ 会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在 timeout 概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的 RabbitMQ 连接断开。这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的 bug——Queue 中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑…
- Message durability如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message 都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ 消息不会丢失。但依然解决不了小概率丢失事件的发生(比如 RabbitMQ 服务器已经接收到生产者的消息,但还没来得及持久化该消息时 RabbitMQ 服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务.
- Prefetch count前面我们讲到如果有多个消费者同时订阅同一个 Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制 Queue每次发 送给每个消费者的消 息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
- Exchange在上一节我们看到生产者将消息投递到 Queue 中,实际上这在 RabbitMQ 中这种事情永远都不会发生。实际的情况是,生产者将消息发送到 Exchange(交换器,下图中的 X),由 Exchange 将消息路由到一个或多个 Queue 中(或者丢弃)。
- routing key生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个 routing key需要与 Exchange Type及 binding key 联合使用才能最终生效。在Exchange Type与 binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给 Exchange时,通过指定 routing key来决定消息流向哪里。RabbitMQ 为 routing key设定的长度限制为255 bytes。
- Binding RabbitMQ 中通过Binding将Exchange与Queue关联起来,这样 RabbitMQ 就知道如何正确地将消息路由到指定的 Queue 了。
- Binding key在绑定(Binding)Exchange与Queue 的同时,一般会指定一个binding key;消费者将消息发送给Exchange 时,一般会指定一个 routing key;当 binding key与routing key相匹配时,消息将会被路由到对应的 Queue 中。这个将在 Exchange Types 章节会列举实际的例子加以说明。在绑定多个 Queue 到同一个 Exchange的时候,这些Binding允许使用相同的binding key。
binding key 并不是在所有情况下都生效,它依赖于 Exchange Type,比如fanout类型的 Exchange 就会无视 binding key,而是将消息路由到所有绑定到该 Exchange 的Queue。
- Exchange Types RabbitMQ常用的Exchange Type有fanout、direct、topic、headers 这四种(AMQP规范里还提到两种 Exchange Type,分别为 system 与自定义,这里不予以描述),下面分别进行介绍。
- Fanout fanout 类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange的消息路由到所有与它绑定的 Queue中。
- direct direct类型的Exchange路由规则也很简单,它会把消息路由到那些 binding key与routing key 完全匹配的 Queue 中。
- topic 前面讲到direct类型的 Exchange 路由规则是完全匹配 binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求.topic 类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到 binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:routing key 为一个句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段 独 立 的 字 符 串 称 为 一 个 单 词 ), 如 “ stock.usd.nyse ” 、 “ nyse.vmw ” 、
“quick.orange.rabbit”binding key 与 routing key 一样也是句点号“. ”分隔的字符串binding key 中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个).
- headers headers类型的Exchange不依赖于 routing key 与 binding key的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定 Queue 与 Exchange 时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全匹配 Queue
与 Exchange 绑定时指定的键值对;如果完全匹配则消息会路由到该 Queue,否则不会路由到该 Queue。该类型的 Exchange 目前用的不多(不过也应该很有用武之地),因此不做重点介绍。
- RPC MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ 后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于 RPC(Remote Procedure Call,远程过程调用)。在 RabbitMQ 中也支持 RPC。
RabbitMQ 中实现 RPC 的机制是:客户端发送请求(消息)时,在消息的属性(MessageProperties,在 AMQP 协议中定义了14种 properties,这些属性会随着消息一起发送)中设置两个值 replyTo
(一个 Queue 名称,用于告诉服务器处理完成后将通知我的消息发送到这个 Queue中)和 correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个 id 了解哪条请求被成功执行了或执行失败)服务器端收到消息并处理,服务器端处理完消息后,将生成一条应答消息到 replyTo 指定的 Queue,同时带上correlationId 属性客户端之前已订阅 replyTo 指定的 Queue,从中收到服务器的应答消息后,根据其中的 correlationId 属性分析哪条请求被执行了,根据执行结果进行后续业务处理.
五.RabbitMQ 案例分析
1.Work queues-工作模式
1.1. 工作模式队列-消息轮询分发(Round-robin)
1.1.1. send类
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 工作模式队列-轮询分发-消息发送者
*/
public class Send {
// 队列名称
private final static String QUEUE_NAME = "work_roundRobin";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
Connection connection = null;
Channel channel = null;
try {
// 通过工厂创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 创建消息
for (int i = 1; i <= 20; i++) {
String message = "Hello World! ----- " + i;
// 将产生的消息放入队列
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 关闭通道
if (null != channel && channel.isOpen())
channel.close();
// 关闭连接
if (null != connection && connection.isOpen())
connection.close();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.1.2. Recv01.java
package com.ego.work.roundRobin.recv;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 工作模式队列-轮询分发-消息接收者
*/
public class Recv01 {
// 队列名称
private final static String QUEUE_NAME = "work_roundRobin";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
Connection connection = factory.newConnection();
// 获取通道
Channel channel = connection.createChannel();
// 指定队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
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] Received01 '" + message + "'");
// 模拟程序执行所耗时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 监听队列
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
1.1.3. Recv02.java
package com.ego.work.roundRobin.recv;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 工作模式队列-轮询分发-消息接收者
*/
public class Recv02 {
// 队列名称
private final static String QUEUE_NAME = "work_roundRobin";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
Connection connection = factory.newConnection();
// 获取通道
Channel channel = connection.createChannel();
// 指定队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
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] Received02 '" + message + "'");
// 模拟程序执行所耗时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 监听队列
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
1.1.4.测试
从结果可以看出消息被平均分配到两个消费方,来对消息进行处理,提高了消息处理率,创建多个消费者来对消息进行处理。这里 RabitMQ 采用轮询来对消息进行分发时保证了消息被平均分配到每个消费方,但是引入新的问题:真正的生产环境下,对于消息的处理
基本不会像我们现在看到的这样,每个消费方处理的消息数量是平均分配的,比如因为网络原因,机器 cpu,内存等硬件问题,消费方处理消息时同类消息不同机器进行处理时消耗时间也是不一样的,比如 1 号消费者消费 1 条消息时 1 秒,2 号消费者消费 1 条消息是 5 秒,对于 1 号消费者比 2 号消费者处理消息快,那么在分配消息时就应该让 1 号消费者多收到消息进行处理,也即是我们通常所说的”能者多劳”,同样 Rabbitmq 对于这种消息分配模式提供了支持。
问题:任务量很大,消息虽然得到了及时的消费,单位时间内消息处理速度加快,提高了吞吐量,可是不同消费者处理消息的时间不同,导致部分消费者的资源被浪费。
解决:采用消息公平分发。
总结:工作队列-消息轮询分发-消费者收到的消息数量平均分配,单位时间内消息处理速度加快,提高了吞吐量。
1.2. 工作模式队列-消息公平分发(fair dispatch)
1.2.1. send类
package com.ego.work.fair.send;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 工作模式队列-公平分发-消息发送者
*/
public class Send {
// 队列名称
private final static String QUEUE_NAME = "work_fair";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
Connection connection = null;
Channel channel = null;
try {
// 通过工厂创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 创建消息
for (int i = 1; i <= 20; i++) {
String message = "Hello World! ----- " + i;
// 将产生的消息放入队列
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 关闭通道
if (null != channel && channel.isOpen())
channel.close();
// 关闭连接
if (null != connection && connection.isOpen())
connection.close();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.2.2. Recv01.java
package com.ego.work.fair.recv;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 工作模式队列-公平分发-消息接收者
*/
public class Recv01 {
// 队列名称
private final static String QUEUE_NAME = "work_fair";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 指定队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
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] Received01 '" + message + "'");
// 模拟程序执行所耗时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
1.2.3. Recv02.java
package com.ego.work.fair.recv;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 工作队列-公平分发-消息接收者
*/
public class Recv02 {
// 队列名称
private final static String QUEUE_NAME = "work_fair";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 指定队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
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] Received02 '" + message + "'");
// 模拟程序执行所耗时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
1.2.4. 测试
从结果可以看出 1 号消费者消费消息数量明显高于 2 号,即消息通过 fair 机制被公平分发到每个消费者。
问题:生产者产生的消息只可以被一个消费者消费,可不可以被多个消费者消费呢?
解决:采用发布与订阅模式。
总结:工作队列-公平轮询分发-根据不同消费者机器硬件配置,消息处理速度不同,收到的消息数量也不同,通常速度快的处理的消息数量比较多,最大化使用计算机资源。适用于生成环境。
1.3. 消息的发布与订阅模式队列(Publish/Subscribe)
1.3.1. send类
package com.ego.publish.subscribe.fanout.send;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 发布与订阅模式队列-fanout 广播模式-消息发送者
*/
public class Send {
// 队列名称
// 如果不声明队列,会使用默认值,RabbitMQ 会创建一个排他队列,连接断开后自动删除
//private final static String QUEUE_NAME = "ps_fanout";
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
Connection connection = null;
Channel channel = null;
try {
// 通过工厂创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 声明队列
//channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定交换机 fanout:广播模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 创建消息,模拟发送手机号码和邮件地址
String message = "18600002222|12345@qq.com";
// 将产生的消息发送至交换机
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 关闭通道
if (null != channel && channel.isOpen())
channel.close();
// 关闭连接
if (null != connection && connection.isOpen())
connection.close();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Receiving
这里对于消费者,消费消息时,消息通过交换机 Exchange 被路由到指定队列,绑定队列
到指定交换机来实现,一个消费者接到消息后用于邮件发送模拟,另一消费者收到消息,用
于短信发送模拟。
1.3.2. Recv01.java
package com.ego.publish.subscribe.fanout.recv;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 发布与订阅模式队列-fanout 广播模式-消息接收者
*/
public class Recv01 {
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 绑定交换机 fanout:广播模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 获取队列名称
String queueName = channel.queueDeclare().getQueue();
// 绑定队列
channel.queueBind(queueName, EXCHANGE_NAME, "");
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
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] Received01 '" + message + "'");
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
1.3.3.Recv02.java
package com.ego.publish.subscribe.fanout.recv;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 发布与订阅模式队列-fanout 广播模式-消息接收者
*/
public class Recv02 {
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 绑定交换机 fanout:广播模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 获取队列名称
String queueName = channel.queueDeclare().getQueue();
// 绑定队列
channel.queueBind(queueName, EXCHANGE_NAME, "");
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
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] Received01 '" + message + "'");
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
1.3.4. 测试
从结果可以看出生产者发送了一条消息,用于邮件发送和短信发送的消费者均可以收到消息进行后续处理。
问题:生产者产生的消息所有消费者都可以消费,可不可以指定某些消费者消费呢?
解决:采用 direct 路由模式。
- Routing-路由模式队列
3.1.1. send类
package com.ego.publish.subscribe.direct.send;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* direct 路由模式队列-消息发送者
*/
public class Send {
// 队列名称
// 如果不声明队列,会使用默认值,RabbitMQ 会创建一个排他队列,连接断开后自动删除
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
Connection connection = null;
Channel channel = null;
try {
// 通过工厂创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 绑定交换机 direct:路由模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 创建消息,模拟收集不同级别日志
String message = "INFO 消息";
//String message = "WARNING 消息";
//String message = "ERROR 消息";
// 设置路由 routingKey
String routingKey = "info";
//String routingKey = "error";
// 将产生的消息发送至交换机
channel.basicPublish(EXCHANGE_NAME, routingKey, null,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 关闭通道
if (null != channel && channel.isOpen())
channel.close();
// 关闭连接
if (null != connection && connection.isOpen())
connection.close();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Receiving
消费者对消息进行后续消费时,对于接收消息的队列在对消息进行接收时,绑定到每一个交换机上的队列均会指定其 Routing Key 规则,通过路由规则将消息路由到执行队列中。消费者 01 routingKey=info 和 warning,对应级别日志消息均会路由到该队列中。
3.1.2. Recv01.java
package com.ego.publish.subscribe.direct.recv;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* direct 路由模式队列-消息接收者
*/
public class Recv01 {
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 绑定交换机 direct:路由模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 获取队列名称
String queueName = channel.queueDeclare().getQueue();
// 设置路由 routingKey
String routingKeyInfo = "info";
String routingKeyWarning = "warning";
// 绑定队列
channel.queueBind(queueName, EXCHANGE_NAME, routingKeyInfo);
channel.queueBind(queueName, EXCHANGE_NAME, routingKeyWarning);
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
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] Received01 '" + message + "'");
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消费者 02 routingKey=error,对应级别日志消息均会路由到该队列中。
3.1.3. Recv02.java
package com.ego.publish.subscribe.direct.recv;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* direct 路由模式队列-消息接收者
*/
public class Recv02 {
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 绑定交换机 direct:路由模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 获取队列名称
String queueName = channel.queueDeclare().getQueue();
// 设置路由 routingKey
String routingKey = "error";
// 绑定队列
channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
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] Received02 '" + message + "'");
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
3.1.4. 测试
从结果可以看出生产者发送了多条设置了路由规则的消息,消费者可以根据具体的路由规则消费对应队列中的消息,而不是所有消费者都可以消费所有消息了。
问题:生产者产生的消息如果场景需求过多需要设置很多路由规则,可不可以减少?
解决:采用 topic 主题模式。
- 主题模式队列(Topics)
4.1.1. send类
package com.ego.publish.subscribe.topic.send;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* topic 主题模式队列-消息发送者
*/
public class Send {
// 队列名称
// 如果不声明队列,会使用默认值,RabbitMQ 会创建一个排他队列,连接断开后自动删除
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
Connection connection = null;
Channel channel = null;
try {
// 通过工厂创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 绑定交换机 topic:主题模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
// 创建消息,模拟商品模块
String message = "商品查询操作";
//String message = "商品更新操作";
// 设置路由 routingKey
String routingKey = "select.goods.byId";
//String routingKey = "update.goods.byId.andName";
// 将产生的消息发送至交换机
channel.basicPublish(EXCHANGE_NAME, routingKey, null,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 关闭通道
if (null != channel && channel.isOpen())
channel.close();
// 关闭连接
if (null != connection && connection.isOpen())
connection.close();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Receiving
4.1.2. Recv01.java
package com.ego.publish.subscribe.topic.recv;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* topic 主题模式队列-消息接收者
*/
public class Recv01 {
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 绑定交换机 topic:主题模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
// 获取队列名称
String queueName = channel.queueDeclare().getQueue();
// 设置路由 routingKey
String routingKey = "select.goods.*";
// 绑定队列
channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
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] Received01 '" + message + "'");
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
4.1.3. Recv02.java
package com.ego.publish.subscribe.topic.recv;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* topic 主题模式队列-消息接收者
*/
public class Recv02 {
// 交换机名称
private static final String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 绑定交换机 topic:主题模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
// 获取队列名称
String queueName = channel.queueDeclare().getQueue();
// 设置路由 routingKey
String routingKey = "update.goods.#";
// 绑定队列
channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
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] Received02 '" + message + "'");
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {
});
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
4.1.4. 测试
从结果可以看出生产者发送了多条设置了路由匹配规则(主题)的消息,根据不同的路由匹配规则(主题),可以将消息根据指定的 routing key 路由到匹配到的队列中,也是在生产中比较常见的一种消息处理方式。
问题:RabbitMQ 本身是基于异步的消息处理,是否可以同步实现?
解决:采用 RPC 模式。
- 远程过程调用模式队列(RPC)
5.1.1.send类
package com.ego.rpc.server;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* RPC 模式队列-服务端
*/
public class RPCServer {
// 队列名称
private static final String RPC_QUEUE_NAME = "rpc_queue";
/**
* 计算斐波那契数列
*
* @param n
* @return
*/
private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.queuePurge(RPC_QUEUE_NAME);
/*
限制 RabbitMQ 只发不超过 1 条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
System.out.println(" [x] Awaiting RPC requests");
Object monitor = new Object();
// 获取消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
// 获取 replyTo 队列和 correlationId 请求标识
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(delivery.getProperties().getCorrelationId())
.build();
String response = "";
try {
// 接收客户端消息
String message = new String(delivery.getBody(), "UTF-8");
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
// 服务端根据业务需求处理
response += fib(n);
} catch (RuntimeException e) {
System.out.println(" [.] " + e.toString());
} finally {
// 将处理结果发送至 replyTo 队列同时携带 correlationId 属性
channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps,
response.getBytes("UTF-8"));
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
// RabbitMq consumer worker thread notifies the RPC server owner thread
// RabbitMq 消费者工作线程通知 RPC 服务器其他所有线程运行
synchronized (monitor) {
monitor.notify();
}
}
};
// 监听队列
/*
autoAck = true 代表自动确认消息
autoAck = false 代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(RPC_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {
});
// Wait and be prepared to consume the message from RPC client.
// 线程等待并准备接收来自 RPC 客户端的消息
while (true) {
synchronized (monitor) {
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
5.1.2.Client
package com.ego.rpc.client;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;
/**
* RPC 模式队列-客户端
*/
public class RPCClient implements AutoCloseable {
private Connection connection;
private Channel channel;
// 队列名称
private String requestQueueName = "rpc_queue";
// 初始化连接
public RPCClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
connection = factory.newConnection();
channel = connection.createChannel();
}
public static void main(String[] args) {
try (RPCClient fibonacciRpc = new RPCClient()) {
for (int i = 0; i < 10; i++) {
String i_str = Integer.toString(i);
System.out.println(" [x] Requesting fib(" + i_str + ")");
// 请求服务端
String response = fibonacciRpc.call(i_str);
System.out.println(" [.] Got '" + response + "'");
}
} catch (IOException | TimeoutException | InterruptedException e) {
e.printStackTrace();
}
}
// 请求服务端
public String call(String message) throws IOException, InterruptedException {
// correlationId 请求标识 ID
final String corrId = UUID.randomUUID().toString();
// 获取队列名称
String replyQueueName = channel.queueDeclare().getQueue();
// 设置 replyTo 队列和 correlationId 请求标识
AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
// 发送消息至队列
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
// 设置线程等待,每次只接收一个响应结果
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
// 接受服务器返回结果
String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
// 将给定的元素在给定的时间内设置到线程队列中,如果设置成功返回 true, 否则返回 false
response.offer(new String(delivery.getBody(), "UTF-8"));
}
}, consumerTag -> {
});
// 从线程队列中获取值,如果线程队列中没有值,线程会一直阻塞,直到线程队列中有值,并且取得该值
String result = response.take();
// 从消息队列中丢弃该值
channel.basicCancel(ctag);
return result;
}
// 关闭连接
public void close() throws IOException {
connection.close();
}
}
5.1.3 测试
查看server
六.RabbitMQ 消息的事物机制
在使用 RabbitMQ 的时候,我们可以通过消息持久化操作来解决因为服务器的异常奔溃导致的消息丢失,除此之外我们还会遇到一个问题,当消息的发布者在将消息发送出去之后,消息到底有没有正确到达 broker 代理服务器呢?如果不进行特殊配置的话,默认情况下发
布操作是不会返回任何信息给生产者的,也就是默认情况下我们的生产者是不知道消息有没有正确到达 broker 的,如果在消息到达 broker 之前已经丢失的话,持久化操作也解决不了这个问题,因为消息根本就没到达代理服务器,你怎么进行持久化,那么这个问题该怎么解决呢?
RabbitMQ 为我们提供了两种方式:
1. 通过 AMQP 事务机制实现,这也是AMQP协议层面提供的解决方案;
2. 通过将channel设置成confirm模式来实现;
1. AMQP 事物机制控制
RabbitMQ 中与事务机制有关的方法有三个:txSelect(), txCommit() 以及txRollback(),txSelect用于将当前channel设置成 transaction模式,txCommit()用于提交事务,txRollback()用于回滚事务,在通过 txSelect()开启事务之后,我们便可以发布消息给 broker 代理服务器了,如果 txCommit()提交成功了,则消息一定到达了broker 了,如果在 txCommit()执行之前 broker 异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过 txRollback()回滚事务。
事务确实能够解决 producer 与 broker 之间消息确认的问题,只有消息成功被 broker接受,事务提交才能成功,否则我们便可以在捕获异常进行事务回滚操作同时进行消息重发,但是使用事务机制的话会降低 RabbitMQ 的性能,那么有没有更好的方法既能保障producer 知道消息已经正确送到,又能基本上不带来性能上的损失呢?从 AMQP 协议的层面看是没有更好的方法,但是 RabbitMQ 提供了一个更好的方案,即将 channel 信道设置成 confirm 模式。
- confirm确认模式
2.1. Confirm确认模式原理:生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker 回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。在channel被设置成confirm 模式之后,所有被 publish 的后续消息都将被confirm(即ack)或者被nack一次。但是没有对消息被 confirm 的快慢做任何保证,并且同一条消息不会既被confirm又被nack。
注意:两种事物控制形式不能同时开启!
- Confirm 确认机制代码实现
实现生产者confirm机制有三种方式:
(1).普通confirm模式:每发送一条消息后,调用 waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm了。
(2).批量confirm模式:每发送一批消息后,调用 waitForConfirmsOrDie()方法,等待服务器端 confirm。
(3). 异步confirm模式:提供一个回调方法,服务端 confirm 了一条或者多条消息后Client 端会回调这个方法。
3.1. 同步 Confirm
SendConfirmSync.java
try {
// 通过工厂创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 开启 confirm 确认模式
channel.confirmSelect();
// 声明队列
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 + "'");
// 确认消息是否发送成功-单条
if (channel.waitForConfirms())
System.out.println("消息发送成功!");
else
System.out.println("消息发送失败!");
// 确认消息是否发送成功-多条
// 直到所有消息都确认,只要有一个未确认就会 IOException
channel.waitForConfirmsOrDie();
System.out.println("消息发送成功!");
}
以上代码可以看出,使用同步的方式需要等所有的消息发送成功以后才会执行后面代码,只要有一个消息未被确认就会抛出 IO 异常。解决办法可以使用异步确认。
3.2 异步 confirm
异步 confirm 模式的编程实现最复杂,Channel对象提供的 ConfirmListener()回调方法只包含deliveryTag(当前Chanel发出的消息序号),我们需要自己为每一个Channel维护一个 unconfirm 的消息序号集合,每publish一条数据,集合中元素加1,每回调一次handleAck方法,unconfirm 集合删掉相应的一(multiple=false)或多条(multiple=true)记录。从程序运行效率上看,这个 unconfirm 集合最好采用有序集合SortedSet存储结构。实际上,waitForConfirms()方法也是通过 SortedSet 维护消息序号的。
SendConfirmAsync.java
package com.ego.tx.send;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.TimeoutException;
/**
* 异步 Confirm 消息确认
*/
public class SendConfirmAsync {
// 队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) {
// 定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setPort(5672);
factory.setHost("127.0.0.1");
factory.setUsername("ego");
factory.setPassword("ego");
factory.setVirtualHost("/ego");
Connection connection = null;
Channel channel = null;
try {
// 维护信息发送回执 deliveryTag
final SortedSet<Long> confirmSet=Collections.synchronizedSortedSet(new TreeSet<Long>());
// 创建连接
connection = factory.newConnection();
// 获取通道
channel = connection.createChannel();
// 开启 confirm 确认模式
channel.confirmSelect();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 添加 channel 监听
channel.addConfirmListener(new ConfirmListener() {
// 已确认
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
// multiple=true 已确认多条 false 已确认单条
if (multiple) {
System.out.println("handleAck--success-->multiple" + deliveryTag);
// 清除前 deliveryTag 项标识 id
confirmSet.headSet(deliveryTag + 1L).clear();
} else {
System.out.println("handleAck--success-->single" + deliveryTag);
confirmSet.remove(deliveryTag);
}
}
// 未确认
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
// multiple=true 未确认多条 false 未确认单条
if (multiple) {
System.out.println("handleNack--failed-->multiple-->" + deliveryTag);
// 清除前 deliveryTag 项标识 id
confirmSet.headSet(deliveryTag + 1L).clear();
} else {
System.out.println("handleNack--failed-->single" + deliveryTag);
confirmSet.remove(deliveryTag);
}
}
});
// 循环发送消息演示消息确认
while (true) {
// 创建消息
String message = "Hello World!";
// 获取 unconfirm 的消息序号 deliveryTag
Long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("utf-8"));
// 将消息序号 deliveryTag 添加至 SortedSet
confirmSet.add(seqNo);
}
} catch (IOException | TimeoutException e) {
e.printStackTrace();
} finally {
try {
// 关闭通道
if (null != channel && channel.isOpen())
channel.close();
// 关闭连接
if (null != connection && connection.isOpen())
connection.close();
} catch (TimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}