SpringBoot从入门到精通系列六:消息组件RabbitMQ
SpringBoot与各种主流消息组件的整合。主流的消息组件大致可以分为3类:
- 基于传统JMS规范的实现,比如经典的ActiveMQ及Artemis
- 基于AMQP协议的消息框组件,如RabbitMQ
- 事件流平台,其代表框架是Kafka
一、面向消息的架构和JMS
面向消息的架构能使分布式应用的两个组件以彻底解耦的方式相互通信,彻底解耦意味着两个组件之间没有任何关联,甚至无须知道对方是否存在。
1.面向消息的架构
消息机制是不同应用程序之间或同一个应用程序的组件之间的通信方法,当一个应用程序或一个组件(该组件被称为生产者)将消息发送到指定的消息目的之后,该消息可以被一个或多个组件(这些组件被称为消费者)读取并处理。
对于面向消息的应用架构来说,消息生成者与消息消费者之间完全隔离,消息生产者只负责将消息发送到消息目的,消息消费者只面向消息目的,消息消费者从消息目的读取并处理消息。
传统消息机制大致有两种模型:
- P2P(点对点)模型,例如电子邮件
- Pub-Sub发布订阅模型,例如BBS
面向消息的架构需要面向消息服务器的支持,消息生产者将消息发送到消息服务器上,消息服务器使用消息队列来保存消息。
- 而消息消费者则通过消息队列来依次读取每条消息,典型的P2P模型。
- 对于Pub-Sub模型而言,消息生产者将消息发送到消息服务器的指定主题下,而消息服务器则将该消息分发给订阅该主题的每个消息消费者。
消息中间件:
- 充当通信资源管理(CRM)的角色,为分布式应用提供实时、高效、可靠、跨操作平台、跨网络系统的消息传递服务
- 消息中间件降低了开发跨平台应用程序的复杂性。
- 在要求可靠传输的系统中,可将消息中间件作为通信平台,向应用程序提供可靠传输消息和文件
消息机制是分布式应用中各组件进行通信的常用方式,使用消息机制可使应用组件之间的通信完全解耦。例如,分布式系统包含订单子系统和库存子系统,当用户下单时,订单子系统就需要通知库存子系统。如果让订单子系统直接调用库存子系统对外暴漏的接口,就可能会产生如下问题:
- 如果库存子系统恰好宕机,库存查询就会失败,从而导致订单创建失败
- 订单子系统与库存子系统耦合,违反了分布式的设计初衷
消息队列也经常用于高并发的流量削峰场景中,例如电商系统中秒杀活动常常由于流量突然暴增导致应用死机。为解决这个问题,可以在应用前端加入消息队列,通过消息队列来控制参加活动的人数,缓解瞬时高流量的系统压力。
面向消息的架构还具有如下优势:
- 消息采用异步处理机制,可以避免客户端等待
- 消息服务器可以持久地保存消息,因而提高了系统的可靠性
- 一条消息可同时发送给多个接收者,这与传统的方法调用有很大的不同,能更好地提高效率
采用异步机制处理消息可以保证消息生产者快速响应,当消息生产者发出一条消息之后,无须等待任何回应即可向下执行。
在面向消息的架构中,消息生产者将消息发送到一个消息目的(既可以是消息主题,也可以是消息队列),而消息消费者则监听或者订阅这个消息目的,因此消息生产者无须等待消息消费者的任何响应,消息生产者也无须阻塞自己的线程来等待新消息消费者。
2.JMS的基础与优势
JMS是Java Message Service的缩写,即Java消息服务。JMS就像一个智能交换机,负责路由分布式应用中各组件所发出的消息。
JMS提供了一组通用的Java应用程序接口API,开发者可以通过这组通用的API来创建、发送、接收、读取消息。
JMS提供了一组基本的API来操作消息系统,JMS系统中大致包含如下常用的API:
- ConnectionFactory(连接工厂):JMS客户端使用连接工厂创建JMS连接
- Connection(连接):表示客户端与服务器之间的活动连接。JMS客户端通过连接工厂创建连接
- session(会话):表示客户端与JMS服务器之间的通信状态。JMS会话建立在连接之上,表示JMS客户端与服务器之间的通信线程。会话定义了消息的顺序,JMS使用会话进行事务性的消息处理。
- Destination(消息目的):即消息生产者发送消息的目的地,也是消息消费者获取消息的消息源。
- MessageProducer(消息生产者):消息生产者负责创建消息并将消息发送到消息目的。
- MessageConsumer(消息消费者):消息消费者负责接收消息并读取消息内容。
3.理解P2P与Pub-Sub
JMS消息机制模型主要分为两类:
P2P(点对点模型):
- JMS将每一条消息只传递给一个消息消费者。
- JMS系统保证消息被传递给消息消费者,消息不会同时被多个消息消费者接收。
- 消息消费者暂时不在连接范围内,JMS会自动保证消息不会丢失,直到消息消费者进入连接范围内,消息将自动送达。
- JMS需要将消息保存到持久性介质上,比如数据库或文件中
对于P2P消息模型而言,消息目的是一个消息队列,消息生产者每次发送消息时总是将消息送入该消息队列中,消息消费者则总是从消息队列中读取消息,先进入队列的消息将先被消息消费者读取。
Pub-Sub(发布-订阅)模型:
- 在这种模型中,每条消息都被发送到一个消息主题下,该主题可以拥有多个订阅者,JMS系统负责将消息的副本分发给该主题的每个订阅者。在发布消息时,默认只会将消息分发给处于在线状态的订阅者,处于离线状态的订阅者将不会收到消息,即使后续该订阅者上线也不会收到之前的消息。
对于Pub-Sub模型的消息系统而言,消息目的是一个消息主题(Topic),消息生产者每次发送消息时就相当于在该主题下发布了一条消息,而该主题的所有订阅者都会收到该消息。
二、整合AMQP
高级消息队列协议(AMQP)是一种与平台无关的、线路级的消息中间件协议。
AMQP和JMS的区别与联系如下:
- JMS定义了消息中间件的规范,从而实现对消息操作的统一、AMQP则通过制定协议来统一数据交互的格式。
- JMS限定了必须使用JAVA语言。AMQP只制定协议,不规定实现语言和实现方式,因此是跨语言的。
- JMS只制订了两种消息模型;而AMQP的消息模型更加灵活。
RabbitMQ就是典型的AMQP产品,是用Erlang语言开发的。从灵活性的角度来看,RabbitMQ比ActiveMQ更优秀。从性能上看,RabbitMQ更是完胜ActiveMQ。优先选择RabbitMQ作为消息队列。
1.RabbitMQ的工作机制
RabbitMQ支持如下核心概念:
- Connection:代表客户端(包括消息生产者和消息消费者)与RabbitMQ之间的连接
- Channel:Channel位于连接内部,负责实际的通信
- Exchange:充当消息交换机的组件
- Queue:消息队列
消息生产者、消息消费者都要通过Connection建立与RabbitMQ之间的连接。客户端与RabbitMQ之间实际通信使用的是Channel(信道),因为RabbitMQ采用了类似于Java NIO的做法,避免为应用程序中的每个线程都建立单独的连接,而是使用Channel来复用连接,不仅可以降低性能开销,而且也便于管理。
应用程序的每个线程都能持有自己对应的Channel,Channel复用了连接,同时RabbitMQ可以确保每个线程的私密性,就像各自拥有独立的连接一样。
- 当每个Channel的数据流量不是很大时,复用单一的连接可以有效地节省连接资源。
- 当Channel本身的数据流量很大时,多个Channel复用一个连接就会产生性能瓶颈,连接本身的流量限制了所有复用它的Channel的总流量,此时可以考虑建立多个连接,并将这些Channel均摊到这些连接中,相关调优策略可根据业务实际情况进行设置。
当消息生产者发送消息时,只需指定如下两个关键信息。
- Exchange:将该消息发送到哪个Exchange
- Routing key:消息的路由key
- RabbitMQ的消息生产者不需要指定将消息发送到哪个消息队列,只需指定将消息发送到哪个Exchange。Exchange相当于消息交换机,它会根据消息的路由key(Routing key)将消息分发到一个或多个消息队列(Queue),消息实际依然由消息队列来负责管理。
- 简单来说,消息生产者将消息发送给Exchange,Exchange负责将消息分发给对应的消息队列,Exchange分发消息的关键在于它本身的类型和路由key。因此,当休息生产者发送消息时,与消息队列是无关的。
- 当消息消费者接收消息时,只需从指定消息队列中获取消息即可,与Exchange是无关的。
RabbitMQ与JMS规范的架构区别在于:
- JMS规范中的消息生产者和消息消费者都是直接与消息目的耦合的,消息生产者向消息目的发送消息,消息消费者从消息目的读取消息。
- RabbitMQ增加了Exchange的概念,通过Exchange对消息生产者与消息消费者做了进一步的隔离,消息生产者向Exchange发送消息,消息消费者从消息队列读取消息,Exchange则负责将消息分发给各消息队列。
为了让Exchange能将信息分发给消息队列,消息队列需要将自己绑定到Exchange上,Exchange只会将消息分发给绑定到自己的消息队列,没有绑定的消息队列不会得到Exchange分发的消息。将消息队列绑定到Exchange时,也需要指定一个路由key。
Exchange就根据发送消息时指定的路由key、绑定消息队列时指定的路由key来决定将消息分发给哪些消息队列。
Exchange的类型也会影响对消息的分发,Exchange可分为以下几类:
- fanout:广播Exchange,这种类型的Exchange会将消息广播到所有与它绑定的消息队列。这种类型的Exchange在分发消息时不看路由key。fanout类型的Exchange大致相当于JMS中的Pub-Sub消息模型
- direct:这种类型的Exchange将消息直接发送到路由key对应的消息队列。
- topic:这种类型的Exchange在匹配路由key时支持通配符。
- headers:这种类型的Exchange要根据消息自带的头信息进行路由。这种类型的Exchange比较少用。
总结来说,Exchange分发消息的逻辑由如下3个因素决定:
- Exchange的类型
- 发送消息时为消息指定的路由key
- 绑定消息队列时所指定的路由key
三、使用默认Exchange支持P2P消息模型
1.创建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>org.crazyit</groupId>
<artifactId>default_exchange</artifactId>
<version>1.0-SNAPSHOT</version>
<name>default_exchange</name>
<properties>
<!-- 定义所使用的Java版本和源代码所用的字符集 -->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
</dependencies>
</project>
上面依赖代表了RabbitMQ Java Client,使用该依赖库开发消息消费者的大致步骤如下:
- 创建connectionFactory,设置连接信息,再通过ConnectionFactory获取Connection。
- 通过Connection获取Channel
- 根据需要调用Channel的queueDeclare()方法声明消息队列,如果声明的队列已经存在,该方法将会直接获取已有的队列。如果声明的队列还不存在,该方法将会创建新的队列。
- 调用Channel的basicConsume()方法开始处理消息,在调用该方法时需要传入一个Consumer参数,该参数相当于JMS中的消息监听器。
对于上面第三步,在大部分场景下都应该显式声明消息队列,这是因为RabbitMQ没有内置的队列,且大部分程序都是创建自动删除的队列,因此通过声明队列可确保所监听的消息队列是存在的。
2.消息消费者程序
\app\message\P2PConsumer.java
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class P2PConsumer
{
final static String QUEUE_NAME = "firstQueue";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
{
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message); // ①
}
};
// 从指定队列中获取消息
channel.basicConsume(QUEUE_NAME, true/* 自动确认 */, consumer);
}
}
- 消费者程序声明了一个名为firstQueue的消息队列
- 然后调用Channel的basicConsume()方法从该消息队列中读取消息
- 从上面程序可以看到,该消息消费者只需从消息队列中读取消息即可,它不需要知道Exchange的存在,与Exchange是完全解耦的
- 确实要获取Exchange信息,消息消费者可通过消息监听方法的Envelope参数来获取
- 由于该程序的最后并未关闭Channel和Connection,因此将一直与RabbitMQ保持连接,除非强制退出该程序。
3.定义工具类用于获取Connection
\app\message\ConnectionUtil.java
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConnectionUtil
{
public static Connection getConnection() throws IOException, TimeoutException
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("32147");
// 如果不设置虚拟主机,则使用默认虚拟主机(/)
// factory.setVirtualHost("fkjava-vhosts");
// 创建与RabbitMQ服务器的TCP连接
return factory.newConnection();
}
}
4.开发消息生产者程序
开发消息生产者程序,使用RabbitMQ Java Client依赖库开发消息生产者程序的大致步骤如下:
- 创建ConnectionFactory,设置连接信息,再通过ConnectionFactory获取Connection
- 通过Connection获取Channel
- 根据需要调用exchangeDeclare()、queueDeclare()方法声明Exchange和消息队列,并完成队列与Exchange的绑定。类似地,声明的Exchange还不存在,则创建该Exchange,否则直接使用已有的Exchange。
- 调用Channel的basicConsume()方法开始处理消息,在调用该方法时需要传入一个Consumer参数,该参数相当于JMS中的消息监听器。
消息生产者程序通常都需要声明Exchange和消息队列,并执行Exchange与消息队列绑定,用于确保该Exchange所分发消息的队列是存在的,且与该Exchange执行了绑定。
注意:
- 对于消息生产者程序,建议总是声明Exchange和消息队列,并执行Exchange与消息队列的绑定,用于确保该Exchange所分消息的队列是存在的,且与该Exchange执行了绑定。
- 对于消息消费者程序,则建议总是声明消息队列,用于确保它监听的消息队列是存在的。
\app\message\P2PProducer.java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class P2PProducer
{
public static void main(String[] args) throws IOException, TimeoutException
{
// 使用自动关闭资源的try语句管理Connection、Channel
try (
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel())
{
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(P2PConsumer.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
for (var i = 1; i < 11; i++)
{
String msg = "第" + i + "条消息";
// 最后一个参数是消息体
channel.basicPublish("", P2PConsumer.QUEUE_NAME/* 路由key */,
null, msg.getBytes(StandardCharsets.UTF_8));
System.out.println("已发送的消息:" + msg);
}
}
}
}
- 声明程序声明了消息队列,用于确保该消息队列的存在,且该消息队列总会自动绑定到默认Exchange。
- 调用了Channel的basicPublish()方法向默认Exchange发送消息,在发送消息时指定了使用P2PConsumer.QUEUE_NAME作为路由key,这意味着该消息将会被分发给与该路由key同名的消息队列
四、工作队列
RabbitMQ可以让多个消息消费者竞争消费同一个消息队列,这种方式被称为工作队列。
当多个消息消费者竞争消费同一个消息队列时,消息队列默认会将消息均分给每个消息消费者,但这样往往不合适,因为有的消息消费者需要更多的时间来处理一条消息,而有的消息消费者只需要更少的时间即可处理一条消息,如果让它们均分这些消息,就会造成资源浪费。
理想做法是:让消息队列将消息多分给需要更少时间的消息消费者,将消息少分给需要更多时间的消息消费者。
channel提供了一个basicQos(int prefetchCount)方法,该方法指定消息消费者在同一时间点最多能得到的消息数量。basicQos(1),意味着每个消息消费者在同一个时间点最多只能得到一条消息。在消息队列收到该消息消费者的确认之前,消息队列不会将新的消息分发给该消息消费者,而是将消息分给其他处于空闲状态(已经返回确认)的消息消费者。
basicQos(1)依赖于消息消费者返回的确认消息,如果采用自动确认策略,程序只要进入消息消费者的handleDelivery()方法,程序就会立即向消息队列发送确认消息,完全不管handleDelivery()方法是否执行完成,甚至不管该方法是否抛出异常。
1.生产者程序
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConnectionUtil
{
public static Connection getConnection() throws IOException, TimeoutException
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("32147");
// 如果不设置虚拟主机,则使用默认虚拟主机(/)
// factory.setVirtualHost("fkjava-vhosts");
// 创建与RabbitMQ服务器的TCP连接
return factory.newConnection();
}
}
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class MyProducer
{
public static void main(String[] args) throws IOException, TimeoutException
{
// 使用自动关闭资源的try语句管理Connection、Channel
try (
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel())
{
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(Consumer1.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
for (var i = 1; i < 11; i++)
{
String msg = "第" + i + "条消息";
// 最后一个参数是消息体
channel.basicPublish("", Consumer1.QUEUE_NAME/* 路由key */,
null, msg.getBytes(StandardCharsets.UTF_8));
System.out.println("已发送的消息:" + msg);
}
}
}
}
2.定义工具类用于获取Connection
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConnectionUtil
{
public static Connection getConnection() throws IOException, TimeoutException
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("32147");
// 如果不设置虚拟主机,则使用默认虚拟主机(/)
// factory.setVirtualHost("fkjava-vhosts");
// 创建与RabbitMQ服务器的TCP连接
return factory.newConnection();
}
}
3.消费者程序
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer1
{
final static String QUEUE_NAME = "firstQueue";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
channel.basicQos(1);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException
{
String message = new String(body, StandardCharsets.UTF_8);
try
{
// 模拟耗时操作
Thread.sleep(1000);
}
catch (InterruptedException e){}
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message);
// 确认消息处理完成
channel.basicAck(envelope.getDeliveryTag(),
false/* 是否同时确认该消息之前的所有未确认的消息 */);
}
};
// 从指定队列中获取消息
channel.basicConsume(QUEUE_NAME, false/* 自动确认 */, consumer);
}
}
- channel.basicQos(1);用于控制该Channel在同一时间点只能处理一条消息
- channel.basicConsume(QUEUE_NAME, false/* 自动确认 */, consumer)取消了消息的自动确认
- channel.basicAck(envelope.getDeliveryTag(),
false/* 是否同时确认该消息之前的所有未确认的消息 */);调用了basicAck()方法手动确认消息,该方法第2个参数指定是否同时确认该消息之前所有未确认的消息。
消费者程序2:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer2
{
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(Consumer1.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 设置该Channel同一时间点只能得到一条消息
channel.basicQos(1);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException
{
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message);
// 确认消息处理完成
channel.basicAck(envelope.getDeliveryTag(),
false/* 是否同时确认该消息之前的所有未确认的消息 */);
}
};
// 从指定队列中获取消息
channel.basicConsume(Consumer1.QUEUE_NAME, false/* 不自动确认 */, consumer);
}
}
- consumer1的handleDelivery()方法中执行了Thread.sleep(1000)来模拟耗时操作,consumer2的handleDelivery()方法没有这行模拟操作的代码
- 先运行Consumer1与Consumer2两个消费者程序,然后运行消息生产者程序发送10条消息,Consumer1分得1条消息,Consumer2分得剩余9条消息
4.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>org.crazyit</groupId>
<artifactId>work_queue</artifactId>
<version>1.0-SNAPSHOT</version>
<name>work_queue</name>
<properties>
<!-- 定义所使用的Java版本和源代码所用的字符集 -->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
</dependencies>
</project>
五、使用fanout实现Pub-Sub消息模型
fanout类型的Exchange不会判断消息的路由key,该Exchange直接将消息分发给绑定到它的所有队列。
消息生产者发送一条消息到fanout类型的Exchange后,绑定到该Exchange的所有队列都会收到该消息的一个副本,消息消费者从不同的队列中读取消息,互不干扰。fanout类型的Exchange可以很好地模拟JMS中的Pub-Sub消息模型。
1.定义工具类获取Connection
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConnectionUtil
{
public static Connection getConnection() throws IOException, TimeoutException
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("32147");
// 如果不设置虚拟主机,则使用默认虚拟主机(/)
// factory.setVirtualHost("fkjava-vhosts");
// 创建与RabbitMQ服务器的TCP连接
return factory.newConnection();
}
}
2.消息生产者程序
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class MyProducer
{
public final static String EXCHANGE_NAME = "fkjava.fanout";
public final static String ROUING_KEY = "test1";
public static void main(String[] args) throws IOException, TimeoutException
{
// 使用自动关闭资源的try语句管理Connection、Channel
try (
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel())
{
// 声明Exchange,指定该Exchange的类型是fanout
channel.exchangeDeclare(EXCHANGE_NAME,
BuiltinExchangeType.FANOUT,
true/*是否持久化*/,
false/*是否自动删除*/, null);
// 声明并绑定两个Queue,如果它们不存在,则自动创建这些Queue
channel.queueDeclare(Consumer1.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
channel.queueBind(Consumer1.QUEUE_NAME,
EXCHANGE_NAME, ROUING_KEY, null);
channel.queueDeclare(Consumer2.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
channel.queueBind(Consumer2.QUEUE_NAME,
EXCHANGE_NAME, ROUING_KEY, null);
for (var i = 1; i < 11; i++)
{
String msg = "第" + i + "条消息";
// 向指定Exchange发送消息,路由key为空字符串
channel.basicPublish(EXCHANGE_NAME, "",
null, msg.getBytes(StandardCharsets.UTF_8));
System.out.println("已发送的消息:" + msg);
}
}
}
}
- 该消息生产者会声明一个类型为fanout的Exchange,并声明两个消息队列,且将者两个消息队列绑定到该Exchange
- 大部分应用中,总是创建自动删除的Exchange,用到Exchange时就声明,声明语句总能确保该Exchange的存在,用完Exchange就自动删除,避免后续引发异常
3.消费者程序
消费者程序比较简单,就是先声明消息队列来确保队列存在,然后调用basicConsume()方法来指定队列中读取消息即可。
Consumer1代码:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer1
{
final static String QUEUE_NAME = "queue1";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
{
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message);
}
};
// 从指定队列中获取消息
channel.basicConsume(QUEUE_NAME, true/* 自动确认 */, consumer);
}
}
Consumer2代码:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer2
{
final static String QUEUE_NAME = "queue2";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
{
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message);
}
};
// 从指定队列中获取消息
channel.basicConsume(QUEUE_NAME, true/* 不自动确认 */, consumer);
}
}
4.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>org.crazyit</groupId>
<artifactId>topic</artifactId>
<version>1.0-SNAPSHOT</version>
<name>topic</name>
<properties>
<!-- 定义所使用的Java版本和源代码所用的字符集 -->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
</dependencies>
</project>
六、使用direct实现消息路由
direct类型的Exchange会根据消息的路由key将消息分发给指定的队列。
- 一个队列能与一个Exchange绑定多个路由key,比如Q2队列与Exchange就绑定了两个路由key:black和green
- RabbitMQ也允许多个队列绑定相同的路由key,又变成了Pub-Sub消息模型。例如Q1和Q2两个队列都绑定了black作为路由key,这意味着若消息生产者发送路由key为black的消息时,该消息将会被分发到Q1和Q2两个队列,这两个队列将会收到各自不同的副本。
- 上面两种绑定方式可以组合起来使用。例如Q1队列绑定了error作为路由key,而Q2队列则绑定了info、error和warning作为路由key,这意味着若消息生产者发送路由key为error的消息时,该消息将会被分发到Q1和Q2两个队列,这两个队列将会同时收到各自的副本。若消息生产者发送路由key为info或warning的消息时,该消息只会被分发到Q2队列,Q1队列不会收到任何消息。
1.定义工具类用于获取Connection
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConnectionUtil
{
public static Connection getConnection() throws IOException, TimeoutException
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("32147");
// 如果不设置虚拟主机,则使用默认虚拟主机(/)
// factory.setVirtualHost("fkjava-vhosts");
// 创建与RabbitMQ服务器的TCP连接
return factory.newConnection();
}
}
2.消息生产者程序
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class MyProducer
{
public final static String EXCHANGE_NAME = "fkjava.direct";
final static String[] ROUING_KEYS = {"info", "warning", "error"};
public static void main(String[] args) throws IOException, TimeoutException
{
// 使用自动关闭资源的try语句管理Connection、Channel
try (
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel())
{
// 声明Exchange,指定该Exchange的类型是direct
channel.exchangeDeclare(EXCHANGE_NAME,
BuiltinExchangeType.DIRECT,
true/*是否持久化*/,
true/*是否自动删除*/, null);
channel.queueDeclare(Consumer1.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 为队列1只绑定1个路由key
channel.queueBind(Consumer1.QUEUE_NAME,
EXCHANGE_NAME, ROUING_KEYS[2], null);
channel.queueDeclare(Consumer2.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 用循环为队列2绑定3个路由key
for (var i = 0; i < ROUING_KEYS.length; i++)
{
channel.queueBind(Consumer2.QUEUE_NAME,
EXCHANGE_NAME, ROUING_KEYS[i], null);
}
for (var i = 1; i < 31; i++)
{
// 根据i的值动态决定路由key
var routingKey = i < 11 ? ROUING_KEYS[0] :
(i < 21 ? ROUING_KEYS[1] : ROUING_KEYS[2]);
String msg = "第" + i + "条消息";
// 向指定Exchange发送消息,路由key为空字符串
channel.basicPublish(EXCHANGE_NAME, routingKey,
null, msg.getBytes(StandardCharsets.UTF_8));
System.out.println("已发送的消息:" + msg);
}
}
}
}
- 程序声明了一个类型为direct的Exchange,会根据消息的路由key将消息分发到不同队列。
3.消费者程序
消费者程序比较简单,就是先声明消息队列来确保队列存在,然后调用basicConsume()方法来指定队列中读取消息即可。
Consumer1代码:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer1
{
final static String QUEUE_NAME = "queue1";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
{
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message);
}
};
// 从指定队列中获取消息
channel.basicConsume(QUEUE_NAME, true/* 自动确认 */, consumer);
}
}
Consumer2代码:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer2
{
final static String QUEUE_NAME = "queue2";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
{
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message);
}
};
// 从指定队列中获取消息
channel.basicConsume(QUEUE_NAME, true/* 不自动确认 */, consumer);
}
}
- 上面的消息生产者发送了30条消息,前10条消息的路由key是info,接下来10条消息的路由key是warning,最后10条消息的路由key是error,这30条消息都会被分发到queue2,因为该队列绑定了3个路由key。只有最后10条消息会被分发到queue1,因为该队列只绑定了这个路由key。
- 先运行本例的MyProducer,再运行Consumer1和Consumer2,将会看到Consumer1仅收到后10条消息,而Consumer收到30条消息。
4.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>org.crazyit</groupId>
<artifactId>topic</artifactId>
<version>1.0-SNAPSHOT</version>
<name>topic</name>
<properties>
<!-- 定义所使用的Java版本和源代码所用的字符集 -->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
</dependencies>
</project>
七、使用topic实现通配符路由
topic类型的Exchange支持在路由key中使用通配符,路由key一般由一个或者多个单词组成,多个单词之间以"."分割。通配符支持*(星号)和#(#号)。
- *:匹配一个单词
- #:匹配零个或多个单词
Q1队列绑定的路由key模式为*.crazyit.*,可以匹配www.crazyit.org、www.crazyit.cn、edu.crazyit.org等路由key。
Q2队列绑定的两个路由key模式为*.org和edu.#,其中*.org可以匹配crazyit.org、fkjava.org等路由key,但不能匹配www.crazyit.org、www.fkjava.org等(*只能匹配一个单词)。而edu.#则可匹配edu.crazyit.org、edu.fkjava.org等路由key(#可匹配多个单词)。
1.定义工具类用于获取Connection
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConnectionUtil
{
public static Connection getConnection() throws IOException, TimeoutException
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("32147");
// 如果不设置虚拟主机,则使用默认虚拟主机(/)
// factory.setVirtualHost("fkjava-vhosts");
// 创建与RabbitMQ服务器的TCP连接
return factory.newConnection();
}
}
2.生产者程序
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class MyProducer
{
public final static String EXCHANGE_NAME = "fkjava.topic";
public final static String[] ROUING_KEYS =
{"www.crazyit.org", "www.crazyit.cn", "edu.crazyit.org",
"edu.fkjava.org", "fkit.org"};
public final static String[] KEY_PATTERNS =
{"*.crazyit.*", "*.org", "edu.#"};
public static void main(String[] args) throws IOException, TimeoutException
{
// 使用自动关闭资源的try语句管理Connection、Channel
try (
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel())
{
// 声明Exchange,指定该Exchange的类型是topic
channel.exchangeDeclare(EXCHANGE_NAME,
BuiltinExchangeType.TOPIC,
true/*是否持久化*/,
false/*是否自动删除*/, null);
channel.queueDeclare(Consumer1.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 为队列1只绑定1个key模式
channel.queueBind(Consumer1.QUEUE_NAME,
EXCHANGE_NAME, KEY_PATTERNS[0], null);
channel.queueDeclare(Consumer2.QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 用循环为队列2绑定2个key模式
for (var i = 1; i < KEY_PATTERNS.length; i++)
{
channel.queueBind(Consumer2.QUEUE_NAME,
EXCHANGE_NAME, KEY_PATTERNS[i], null);
}
for (var i = 0; i < ROUING_KEYS.length; i++)
{
String msg = "第" + (i + 1) + "条消息";
// 向指定Exchange发送消息,路由key为空字符串
channel.basicPublish(EXCHANGE_NAME, ROUING_KEYS[i],
null, msg.getBytes(StandardCharsets.UTF_8));
System.out.println("已发送的消息:" + msg);
}
}
}
}
- 声明了一个类型为topic的Exchange,可以支持根据路由key的模式匹配将消息分发到不同队列。
- 生产者根据ROUING_KEYS数组发送了5条消息:数组有几个元素,该程序就会发送几条消息,每条消息使用不同的路由key。
3.消费者程序
Consumer1:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer1
{
final static String QUEUE_NAME = "queue1";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
{
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message);
}
};
// 从指定队列中获取消息
channel.basicConsume(QUEUE_NAME, true/* 自动确认 */, consumer);
}
}
Consumer2:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Consumer2
{
final static String QUEUE_NAME = "queue2";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明Queue,如果该Queue不存在,会自动创建该Queue
channel.queueDeclare(QUEUE_NAME, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */, null);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
{
String message = new String(body, StandardCharsets.UTF_8);
System.out.println(envelope.getExchange() + ","
+ envelope.getRoutingKey() + "," + message);
}
};
// 从指定队列中获取消息
channel.basicConsume(QUEUE_NAME, true/* 不自动确认 */, consumer);
}
}
4.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>org.crazyit</groupId>
<artifactId>topic</artifactId>
<version>1.0-SNAPSHOT</version>
<name>topic</name>
<properties>
<!-- 定义所使用的Java版本和源代码所用的字符集 -->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
</dependencies>
</project>
八、RPC通信模型
通过使用两个独享队列,可以让RabbitMQ实现RPC(远程过程调用)通信模型,其通信过程其实很简单:
- 客户端向服务器消费的独享队列发送一条消息,服务器收到该消息后,对该消息进行处理,然后将处理结果发送给客户端消费的独享队列
- 使用独享队列可以避免其他连接读取该队列中的消息,只有当前连接才能读取该队列中的消息,这样才可以保证服务器能读到客户端发送的每条消息,客户端也能读到服务器返回的每条消息。
为了让服务器知道客户端所消费的独享队列,客户端发送消息时,应该将自己监听的队列名以reply_to参数发送给服务器。为了能准确识别服务器应答消息与客户端请求消息之间的对应关系,还需要为每条消息都增加一个correlation_id属性,两条具有相同的correlation_id属性值的消息,可认为它们是配对的两条消息。
流程说明:
- 服务器启动时,会创建一个名为rpc_queue的独享队列(名称可以随意),并使用服务器端的消费者监听该独享队列的消息。
- 客户端启动时,它会创建一个匿名的独享队列(由RabbitMQ命名),并使用客户端的消费者监听该独享队列的消息
- 客户端发送带有两个属性的消息:一个是代表应答队列名的reply_to属性;另一个是代表消息标识的correlation_id属性。
- 将消息发送到服务器监听的rpc_queue队列中。
- 服务器从rpc_queue队列中读取消息,服务器调用处理程序对该消息进行计算,将计算结果以消息的形式发送给reply_to属性指定的队列,并为消息添加correlation_id属性。
- 客户端从reply_to对应的队列中读取消息,当消息出现时,它会检查消息的correlation_id属性。如果此属性的值与请求消息的correlation_id属性的值匹配,则将它返回给应用。
1.定义工具类用于获取Connection
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConnectionUtil
{
public static Connection getConnection() throws IOException, TimeoutException
{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("root");
factory.setPassword("32147");
// 如果不设置虚拟主机,则使用默认虚拟主机(/)
// factory.setVirtualHost("fkjava-vhosts");
// 创建与RabbitMQ服务器的TCP连接
return factory.newConnection();
}
}
2.服务器程序Server.java
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Server
{
public static final String SERVER_QUEUE = "rpc_queue";
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明服务器消费的独享队列,用于创建该队列
channel.queueDeclare(SERVER_QUEUE, true,
true, true, null);
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
// 每当读到消息队列中的消息时,该方法将会被自动触发
// Envelope参数代表信息封包,可获得Exchange名,和路由key
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException
{
int number = Integer.parseInt(new String(body, StandardCharsets.UTF_8));
// 获取发送应答消息的队列
var replyQueue = properties.getReplyTo();
// 获取correlation_id属性
var correlationId = properties.getCorrelationId();
// 向默认Exchange发送消息,使用replyQueue作为路由key,
// 该消息将被分发给replyQueue队列
channel.basicPublish("", replyQueue,
new AMQP.BasicProperties.Builder()
.correlationId(correlationId + "")
.build(),
// 以服务器计算的的结果作为消息体
(cal(number) + "").getBytes(StandardCharsets.UTF_8));
}
};
// 读取SERVER_QUEUE队列的消息
channel.basicConsume(SERVER_QUEUE, true, consumer);
}
// 模拟服务器端的计算功能
public static int cal(int n)
{
int result = 1;
for (var i = 2; i <= n; i++)
{
result *= i;
}
return result;
}
}
- 声明了一个独享队列,该服务器程序将会等待消费该队列中的消息,其他连接无法读取该队列中的消息
- 程序定义了一个Consumer用于监听该队列中的消息,当Consumer收到客户端的消息之后,从消息中取出reply_to和correlation_id两个属性
- 然后将服务器计算结果以消息的形式返回给reply_to队列,并在消息中添加correlation_id属性。
3.客户端程序
本例直接使用RabbitMQ的默认Exchange,该Exchange默认就是持久化的,且服务器监听的队列也是持久化的,设置了消息的分发模式2(2代表持久化),这样保证使用持久化的消息,从而确保客户端发送的消息不会丢失。
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Client
{
public static void main(String[] args) throws IOException, TimeoutException
{
// 创建与RabbitMQ服务器的TCP连接
Connection connection = ConnectionUtil.getConnection();
// 创建Channel
Channel channel = connection.createChannel();
// 声明一个由RabbitMQ命名的、独享的、会自动删除的队列
String replyQueue = channel.queueDeclare().getQueue();
// 创建消费者
Consumer consumer = new DefaultConsumer(channel)
{
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
{
String message = new String(body, StandardCharsets.UTF_8);
// 取出correlationId,用于获取发出消息的ID
var correlationId = properties.getCorrelationId();
System.out.println(correlationId + "返回的消息为:"
+ message);
}
};
// 等待从指定队列获取消息
channel.basicConsume(replyQueue, true, consumer);
// 采用循环发送10条消息
for (var i = 1; i < 10; i++)
{
// 使用AMQP.BasicProperties封装消息属性
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.replyTo(replyQueue)
.correlationId(i + "")
.deliveryMode(2) // 使用持久化消息
.build();
// 向默认Exchange发送消息,使用rpc_queue作为路由key
channel.basicPublish("", Server.SERVER_QUEUE,
props, (i + "").getBytes(StandardCharsets.UTF_8));
}
}
}
- String replyQueue = channel.queueDeclare().getQueue();声明了一个由RabbitMQ命名的独享队列
- channel.basicConsume(replyQueue, true, consumer);使用Consumer等待消费该独享队列中的消息,可以保证所有服务器返回的应答消息都由该客户端负责处理
- channel.basicPublish向服务器监听的队列发送消息,在发送消息时指定reply_to和correlation_id两个属性,其中reply_to属性代表客户端所监听的独享队列,correlation_id属性代表请求消息的唯一标识。
- 先运行本例的Server程序,再运行Client程序,可以在客户端看到通过消息机制远程调用服务器方法(RPC)的效果。
九、SpringBoot与RabbitMQ支持
SpringBoot提供了一个spring-boot-starter-amqp的Starter来支持RabbitMQ,只要添加该Startter,就会添加spring-rabbit依赖库。SpringBoot基于标准的AMQP与RabbitMQ进行通信。
SpringBoot检测到类加载路径下包含spring-rabbit依赖库,就会自动配置CachingConnectionFactory,还会自动配置AmqpAdmin和AmqpTemplate
- AmqpAdmin:管理Exchange、队列和绑定
- AmqpTemplate:发送、接收消息
2.AmqpAdmin和AmqpTemplate
AmqpAdmin常用方法:
- void declareExchange(Exchange exchange):声明Exchange
- String declareQueue(Queue queue):声明队列
- Queue declareQueue():声明由服务器命名的、独享的、会自动删除的、非持久化的队列
- declareBinding(Binding binding):声明队列或Exchange与Exchange的绑定
- boolean deleteExchange(String exchangeName):删除Exchange
- boolean deleteQueue(String queueName):无条件地删除duilie
- void deleteQueue(String queueName,boolean unused,boolean empty):删除队列,只有当该队列不再使用且没有消息时才删除
- void removeBinding(Binding binding):解除绑定
AmqpTemplate常用方法:
- convertAndSend(String exchange,String routingKey,Object message,MessagePostProcessor messagePostProcessor):自动将message参数转换成消息发送给exchange。在发送之前,还可通过messagePostProcessor参数对消息进行修改。
- convertSendAndReceive(String exchange,String routingKey,Object message,MessagePostProcessor messagePostProcessor):该方法在发送消息之后等待返回的消息,在发送之前,还可通过messagePostProcessor参数对消息进行修改。
- send(String exchange,String routingKey,Message message):发送消息
- sendAndReceive(String exchange,String routingKey,Message message):该方法在发送消息之后会等待返回的消息。
- receive(String queueName,long timeoutMillis):指定从queueName队列中接收消息
2.配置RabbitMQ
## 设置RabbitMQ的主机和端口
#spring.rabbitmq.host=localhost
#spring.rabbitmq.port=5672
## 设置用户名和密码
#spring.rabbitmq.username=root
#spring.rabbitmq.password=32147
spring.rabbitmq.addresses=amqp://root:32147@localhost:5672
# 设置虚拟主机
spring.rabbitmq.virtual-host=fkjava-vhost
# 设置要缓存Channel还是缓存Connection
spring.rabbitmq.cache.connection.mode=channel
# 设置缓存Channel的数量
spring.rabbitmq.cache.channel.size=20
# 设置缓存Connection的数量
#spring.rabbitmq.cache.connection.size=5
# 启用AmqpTemplate的自动重试
spring.rabbitmq.template.retry.enabled=true
# 设置自动重试的时间间隔为2秒
spring.rabbitmq.template.retry.initial-interval=2s
# 设置AmqpTemplate默认的Exchange为""
spring.rabbitmq.template.exchange=""
# 设置AmqpTemplate默认的路由key为"test"
spring.rabbitmq.template.routing-key=test
3.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>
<!-- 指定继承spring-boot-starter-parent POM文件 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<groupId>org.crazyit</groupId>
<artifactId>rabbitmq_boot</artifactId>
<version>1.0-SNAPSHOT</version>
<name>rabbitmq_boot</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加Spring Boot AMQP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 定义Spring Boot Maven插件,可用于运行Spring Boot应用 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.使用AmqpTemplate发送消息Service组件
SpringBoot可以将AmqpAdmin和AmqpTemplate注入任何其他组件,接下来该组件即可通过AmqpAdmin来管理Exchange、队列和绑定,还可通过AmqpTemplate来发送消息。
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MessageService
{
public static final String EXCHANGE_NAME = "boot.fanout";
public static final String[] QUEUE_NAMES = {"myQueue1", "myQueue2"};
private final AmqpAdmin amqpAdmin;
private final AmqpTemplate amqpTemplate;
@Autowired
public MessageService(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate)
{
this.amqpAdmin = amqpAdmin;
this.amqpTemplate = amqpTemplate;
// 创建Exchange对象,根据Exchange类型的不同
// 可使用DirectExchange、FanoutExchange、
// HeadersExchange、TopicExchange不同的实现类
var exchange = new FanoutExchange(EXCHANGE_NAME,
true/* 是否持久化 */, true/* 是否自动删除 */);
// 声明Exchange
this.amqpAdmin.declareExchange(exchange);
// 使用循环声明、并绑定了两个队列
for (String queueName : QUEUE_NAMES)
{
var queue = new Queue(queueName, true/* 是否持久化 */,
false/* 是否独享 */, true/* 是否自动删除 */);
// 声明队列
this.amqpAdmin.declareQueue(queue);
var binding = new Binding(queueName,
Binding.DestinationType.QUEUE/* 指定绑定的目的为队列 */,
EXCHANGE_NAME, ""/* 路由key */, null);
// 声明绑定
this.amqpAdmin.declareBinding(binding);
}
}
public void produce(String message)
{
this.amqpTemplate.convertAndSend(EXCHANGE_NAME,
""/* 路由key */, message);
}
}
- this.amqpAdmin.declareExchange(exchange);使用AmqpAdmin声明Exchange
- this.amqpAdmin.declareQueue(queue);声明队列
- this.amqpAdmin.declareBinding(binding);声明Exchange与队列之间的绑定
- this.amqpTemplate.convertAndSend使用AmqpTemplate发送消息
5.使用控制器类调用Service组件的方法
import org.crazyit.app.service.MessageService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController
{
private final MessageService messService;
public HelloController(MessageService messService)
{
this.messService = messService;
}
@GetMapping("/produce/{message}")
public String produce(@PathVariable String message)
{
messService.produce(message);
return "发送消息";
}
}
6.接收消息
SpringBoot会自动将@RabbitListener注解修饰的方法注册为消息监听器。如果没有显式配置监听器容器工厂(RabbitListenerContainerFactory),SpringBoot会在容器中自动配置一个SimpleRabbitListenerContainerFactory Bean作为监听器容器工厂。如果希望使用DirectRabbit-ListenerContainerFactory,则可在application.properties文件中添加如下配置:
spring.rabbitmq.listener.type=direct
如果容器中配置了MessageRecoverer或MessageConverter,它们会被自动关联到默认的监听器容器工厂。
监听消息队列的监听器:
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class QueueListener1
{
@RabbitListener(queues = "myQueue1")
public void processMessage(String content)
{
System.out.println("从myQueue1收到消息:" + content);
}
}
- 上面processMessage()方法使用了@RabbitListener(queues=“myQueue1”)注解修饰,表明该方法将会监听myQueue1消息队列。
7.自定义更多的监听器容器工厂
如果要定义更多的监听器容器工厂或者覆盖默认的监听器容器工厂,则可通过SpringBoot提供的SimpleRabbitListenerContainerFactoryConfigure或DirectRabbitListenerContainerFactoryConfigurer来实现,它们可对SimpleRabbitListenerContainerFactory或DirectRabbitListenerContainerFactory进行与自动配置相关的设置。
例如以下配置片段:
@Configuration(proxyBeanMethods = flase)
static class RabbitConfiguration
{
@Bean
public SimpleRabbitListenerContainerFactory myFactory(
SimpleRabbitListenerContainerFactoryConfigurer configurer,
ConnectionFactory connectionFactory)
{
//创建SimpleRabbitListererContainerFactory实例
SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
//使用与自动配置相同的属性来配置监听器容器工厂
configurer.configure(factory,connectionFactory);
//下面可对SimpleRabbitListenerContainerFactory进行任意额外的设置
...
return factory;
}
}
- configurer.configure(factory,connectionFactory);进行了与自动配置相同的设置,简单来说,就得到了一个与自动配置的SimpleRabbitListenerContainerFactory具有相同设置的实例,接下来可对该实例进行额外的定制。
- 有了自定义的监听器容器工厂之后,可通过@RabbitListener注解的containerFactory属性来指定使用自定义的监听容器工厂,例如以下注解代码:
@RabbitListener(queues="myQueue2",containerFactory="myFactory")
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class QueueListener2
{
@RabbitListener(queues = "myQueue2",containerFactory="myFactory")
public void processMessage(String content)
{
System.out.println("从myQueue2收到消息:" + content);
}
}
8.主程序
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
SpringApplication.run(App.class, args);
}
}
- 只要调用SpringApplication的run()方法启动SpringBoot应用即可。
- 运行主类启动SpringBoot应用,使用浏览器访问http://localhost:8080/produce/fkjava即可向服务器发送消息。
- 由于该消息被发送到fanout类型的Exchange,因此该消息将会被分发到该Exchange的两个队列,所以可在控制台看到两个消息监听器(分别监听myQueue1和myQueue2)都收到了消息。