后端接收到大量请求后,由于服务资源有限,服务端并不希望马上处理该请求。要解决这个问题,就可以使用消息服务中间件,就让请求等一会吧。
RabbitMQ 是什么
MQ 是 Message Queue 的缩写,意为消息队列,是一种应用程序对应用程序的通信方式。
RabbitMQ 是 MQ 中的一种,它采用 Erlang 语言开发,并实现了 AMQP 协议,是被广泛使用的开源消息中间件。
RabbitMQ 的特点为性能良好、延时低、拥有友好的管理界面,但缺点是吞吐量较低(只达到万级,而 Kafka 可达十万级甚至百万级)。
RabbitMQ 安装
请参考 RabbitMQ 3.12 安装教程 进行安装即可。
RabbitMQ 使用场景
- 流量削峰
- 日志处理
- 应用解耦
- 异步处理
AMQP 协议
AMQP 的全程为 Advanced Message Queuing Protocol,即高级消息队列协议。
在图中,生产者和消费者应当共用一个 connection 单例,这里只是为了方便理解才画成了两个。
Java 操作 RabbitMQ
为了让后面的知识点更容易被初学者理解,让我们先实现一个 “简单的生产与消费” 案例。它们均依赖于 amqp-client-5.1.2.jar :
-
工具类
import com.rabbitmq.client.*; public final class RabbitmqUtil { static final ConnectionFactory factory; static final Connection connection; static { // 创建连接工厂 factory = new ConnectionFactory(); // 设置 RabbitMQ 服务地址 factory.setHost("0.0.0.0"); // 设置 RabbitMQ 服务端口 factory.setPort(5672); // 设置账号信息: vhost、用户名、密码 factory.setVirtualHost("/"); factory.setUsername("guest"); factory.setPassword("guest"); // 通过工厂创建连接 try { connection = factory.newConnection(); } catch (IOException e) { throw new RuntimeException(e); } catch (TimeoutException e) { throw new RuntimeException(e); } } public static Connection getConnection() { return connection; } } -
生产者
import com.rabbitmq.client.*; public class Producer { private final static String QUEUE_NAME = "queue_ego"; public static void main(String[] args) throws IOException, TimeoutException { try ( // 获取 RabbitMQ 连接 Connection connection = RabbitmqUtil.getConnection(); // 在连接中创建通道 Channel channel = connection.createChannel(); ) { /** * 为通道声明并创建队列(如果队列已存在,则直接使用已有队列),各参数的含义: * 1. 队列名称 * 2. 是否持久化 * 3. 是否为独占模式 * 4. 是否自动删除队列中的消息,为 true 则在断开连接后删除消息 * 5. 额外参数 */ channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 定义消息内容 String message = "Hello World!"; /** * 将消息内容发送至队列,各参数的含义: * 1. 交换机名称 * 2. 队列名称 * 3. BasicProperties 实例 * 4. 消息字符数组 */ channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println("已发送: " + message); } } } -
消费者
import com.rabbitmq.client.*; public class Consumer { private final static String QUEUE_NAME = "queue_ego"; public static void main(String[] args) throws IOException, TimeoutException { try ( // 获取 RabbitMQ 连接 Connection connection = RabbitmqUtil.getConnection(); // 在连接中创建通道 Channel channel = connection.createChannel(); ) { /** * 为通道声明并创建队列(如果队列已存在,则直接使用已有队列),各参数的含义: * 1. 队列名称 * 2. 是否持久化 * 3. 是否为独占模式 * 4. 是否自动删除队列中的消息,为 true 则在断开连接后删除消息 * 5. 额外参数 */ channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 定义队列的消费者 DefaultConsumer dc = new DefaultConsumer(channel) { /** * 处理接收到的消息 * @param consumerTag 同一个会话,consumerTag 是固定的,即作为会话的名字 * @param envelope 可通过该对象获取当前消息的编号、发送的队列、交换机信息 * @param properties 随消息一起发送的其他属性 * @param body 消息内容 * @throws IOException */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body); System.out.println("已接收: " + message); } }; /** * 为消费者创建一个监听器,各参数的含义: * 1. 队列名称 * 2. 是否自动确认收到消息,为 false 则需要手动确认 * 3. 消费者句柄 */ channel.basicConsume(QUEUE_NAME, true, dc); // 为了持续监听消息,阻塞当前线程,否则程序会自动结束并退出 while (true) { Thread.sleep(1000); } } } }
我们先运行消费者,再运行生产者,即可查看到生产者生产的 "Hello World!" 被消费者消费了。有了这个基本的生产消费概念,我们就可以进入比较复杂的 RabbitMQ 消息类型 章节了。
RabbitMQ 消息类型
-
hello
graph LR A((P)) --> B[[Queue]] B --> C((C)) style A fill:#A9DEF9 style B fill:#EDE7B1 style C fill:#D3F8E2一个生产者把消息发送给一个队列,该队列只被一个消费者所使用,采用该消息类型。
代码请查看前面的的 Java 操作 RabbitMQ。
-
work
graph LR A((P)) --> B[[Queue]] B --> C1(("C₁")) B --> C2(("C₂")) style A fill:#A9DEF9 style B fill:#EDE7B1 style C1 fill:#D3F8E2 style C2 fill:#D3F8E2一个生产者把消息发送给一个队列,该队列只被多个消费者所使用,采用该消息类型。
与 “简单的生产与消费” 相比,消费者需要做以下设置:
// ... // 设置消费者预取的消费数量(被预取的消息如果未处理完,则不再从消息队列中获取更多消息) channel.basicQos(1); // ... DefaultConsumer dc = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // ... /** * 手动返回一个回执确认,参数含义: * 1. 需要回执确认的消息编号 * 2. 是否批量确认 */ channel.basicAck(envelope.getDeliveryTag(), false); } }; // ... // 不再自动确认收到消息,改为在 handleDelivery 方法中手动确认 channel.basicConsume(QUEUE_NAME, false, dc); -
fanout
graph LR A((P)) --> X{{X}} X --> Q1[["Q₁"]] X --> Q2[["Q₂"]] Q1 --> C1(("C₁")) Q2 --> C2(("C₂")) style A fill:#A9DEF9 style X fill:#FAE0E4 style Q1 fill:#EDE7B1 style Q2 fill:#EDE7B1 style C1 fill:#D3F8E2 style C2 fill:#D3F8E2一个生产者把消息发送给交换机,交换机再直接分发给不同的队列,采用该消息类型。
之前的消息种类对应的代码,消息内容都是生产者直接发送到消息队列上。在该消息种类中的代码,消息内容是由生产者发送到交换机上(由交换机分发给不同的消息队列):
private final static String EXCHANGE_NAME = "exchange_fanout"; // ... // 声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); /** * 将消息内容发送至交换机,各参数的含义: * 1. 交换机名称 * 2. 队列名称 * 3. BasicProperties 实例 * 4. 消息字符数组 */ channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes()); System.out.println("已发送: " + message);消费者则需要声明队列,并将队列绑定到交换机上
// ... // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 将队列绑定到交换机 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); // 设置消费者预取的消费数量 channel.basicQos(1); // ... -
direct
graph LR A((P)) --> X{{X}} X -- a --> Q1[["Q₁"]] X -- a --> Q2[["Q₂"]] X -- b --> Q2[["Q₂"]] X -- c --> Q2[["Q₂"]] Q1 --> C1(("C₁")) Q2 --> C2(("C₂")) style A fill:#A9DEF9 style X fill:#FAE0E4 style Q1 fill:#EDE7B1 style Q2 fill:#EDE7B1 style C1 fill:#D3F8E2 style C2 fill:#D3F8E2一个生产者把消息发送给交换机,交换机根据消息的 routingKey 分发给不同的队列(一个队列可以指定多个 routingKey,一个 routingKey 可以被多个队列同时指定),采用该消息类型。该消息类型对应的交换机模式又叫路由模式。
生产者在声明交换机时,使用 "direct" 参数:
private final static String EXCHANGE_NAME = "exchange_direct"; // ... // 声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, "direct"); /** * 将消息内容发送至交换机,各参数的含义: * 1. 交换机名称 * 2. 队列名称(或者 队列绑定的路由名称之一) * 3. BasicProperties 实例 * 4. 消息字符数组 */ channel.basicPublish(EXCHANGE_NAME, "info", null, message.getBytes()); // ...消费者则需要声明队列时指定路由名称,并将队列绑定到交换机上
// ... // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 将队列绑定到交换机(多个则表示要绑定不同的 routingKey) channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error"); // 设置消费者预取的消费数量 channel.basicQos(1); // ... -
topic
graph LR A((P)) --> X{{X}} X -- *.a.* --> Q1[["Q₁"]] X -- *.*.b --> Q2[["Q₂"]] X -- c.# --> Q2[["Q₂"]] Q1 --> C1(("C₁")) Q2 --> C2(("C₂")) style A fill:#A9DEF9 style X fill:#FAE0E4 style Q1 fill:#EDE7B1 style Q2 fill:#EDE7B1 style C1 fill:#D3F8E2 style C2 fill:#D3F8E2一个生产者把消息发送给交换机,交换机根据消息的 topic 分发给不同的队列(一个队列可以指定多个 topic),采用该消息类型。该消息类型对应的交换机模式又叫 topics 模式。
在 topics 串中,
*代表一个标记符(单词),#代表多个标记符(单词)用点号分隔。生产者在声明交换机时,使用 "topic" 参数:
private final static String EXCHANGE_NAME = "exchange_topic"; // ... // 声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, "topic"); /** * 将消息内容发送至交换机,各参数的含义: * 1. 交换机名称 * 2. 队列名称(或者 队列绑定的某个 topic) * 3. BasicProperties 实例 * 4. 消息字符数组 */ channel.basicPublish(EXCHANGE_NAME, "cc.123", null, message.getBytes()); // ...消费者则需要声明队列时指定 topic 名称,并将队列绑定到交换机上
// ... // 声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 将队列绑定到交换机(多个则表示要绑定不同的 topic) channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "cc.*"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "#.ego"); // 设置消费者预取的消费数量 channel.basicQos(1); // ... -
RPC
graph LR A((Client)) -- request --> Q1[[RPC]] Q1 --> C((Server)) C --> Q2[[Reply]] Q2 -- reply --> A style A fill:#A9DEF9 style Q1 fill:#EDE7B1 style Q2 fill:#EDE7B1 style C fill:#D3F8E2Client 发起请求时,会携带 reply_to 和 correlation_id 参数,其中 reply_to 是 Server 识别 Client 的唯一标识。correlation_id 则是每个请求的唯一标识。
在该类型下,生产者作为客户端,不再声明交换机,而是创建回调队列:
/** * 类名不再是 Producer,而是 Client */ // 定义位于 Server 的队列名 private final static String RPC_QUEUE_NAME = "rpc_queue"; // ... // 创建回调队列 String replyQueue = channel.queueDeclare().getQueue; // 客户端作为消费者时从回调队列中接收服务端传送的消息 QueueingConsumer consumer = new QueueingConsumer(channel); // 监听回调队列(把 consumer 绑定到回调队列) channel.basicConsume(replyQueue, true, consumer); // 创建带有 correlation_id 的消息属性 String correlationId = UUID.randomUUID().toString(); AMQP.BasicProperties basicProperties = new AMQP.BasicProperties() .builder() .correlationId(correlationId) .replyTo(replyQueue) .build(); String message = "Hi from client"; /** * 将消息内容发送至交换机,各参数的含义: * 1. 交换机名称 * 2. 队列名称 * 3. BasicProperties 实例 * 4. 消息字符数组 */ channel.basicPublish("", RPC_QUEUE_NAME, basicProperties, message.getBytes()); // ... // 接收回调消息 while (true) { QueueingConsumer.Delivery delivery = consumer.nextDelivery(); String receivedCorrelationId = delivery.getProperties().getCorrelationId(); if (correlationId.equals(receivedCorrelationId)) { System.out.println("获得 Server 的回调消息:" + new String(delivery.getBody())); break; } }在该类型下,消费者作为服务端。
/** * 类名不再是 Consumer,而是 Server */ // 定义当前的队列名 private final static String RPC_QUEUE_NAME = "rpc_queue"; // ... // 声明队列 channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null); // 设置消费者预取的消费数量 channel.basicQos(1); // 服务端作为消费者时从回调队列中接收客户端传送的消息 QueueingConsumer consumer = new QueueingConsumer(channel); // 监听队列(把 consumer 绑定到当前队列上) channel.basicConsume(RPC_QUEUE_NAME, false, consumer); // 接收客户端消息 while (true) { QueueingConsumer.Delivery delivery = consumer.nextDelivery(); System.out.println("获得 Client 的发送消息:" + new String(delivery.getBody())); // 确认收到消息 channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); // 取出消息的 correlation_id AMQP.BasicProperties properties = delivery.getProperties(); String correlationId = properties.getCorrelationId(); // 创建具有与接收消息相同的 correlation_id 的消息属性 AMQP.BasicProperties replyProperties = new AMQP.BasicProperties() .builder() .correlationId(correlationId) .build(); // properties.getReplyTo() 可获得回调队列名(Client 创建的) channel.basicPublish("", properties.getReplyTo(), replyProperties, "Good!".getBytes()); }
SpringBoot 集成 RabbitMQ
首先,在项目根目录下的 pom 文件中引入 amqp 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
然后在配置模块的资源目录(如 config/src/main/resources)中的配置文件 bootstrap.yml 中加入以下配置:
spring:
rabbitmq:
host: localhost
port: 5672
virtual-host: /
username: guest
password: guest
如果您使用的配置文件不是 bootstrap.yml,而是 application.properties,则添加以下配置:
spring.rabbitmq.host: localhost
spring.rabbitmq.port: 5672
spring.rabbitmq.virtual-host: /
spring.rabbitmq.username: guest
spring.rabbitmq.password: guest
接下来就是在生产者代码引入 AmqpTemplate ,消费者代码引入 RabbitListener 和 RabbitHandler 即可:
-
生产者
@Component import org.springframework.amqp.core.AmqpTemplate; public class MessageSender { @Autowired private AmqpTemplate rabbitmqTemplate; public void send() { String message = "Hello from sender"; this.rabbitmqTemplate.convertAndSend("queue_name", message); } // ... } -
消费者
import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.annotation.RabbitHandler; @Component @RabbitListener(queues = "queue_name") public class MessageReceiver { @RabbitHandler public void process(String message) { System.out.println("已接收: " + message); } // ... } -
测试代码
@SpringBootTest public class RabbitmqTest { @Autowired private MessageSender messageSender; @Test public void sayHi() throws Exception { messageSender.send(); } }
小结
本文简单地介绍了 RabbitMQ 是什么、有啥用、如何用。如果您是初学者,那本文应该算个不错的入门教程。至于更复杂的使用、RabbitMQ 的底层原理、搭建 RabbitMQ 集群等进阶知识,请等待未来的文章。