# 😎 知识点概览
> 为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 `day05` 的内容
- [x] `RabbitMQ` 的基本应用场景
- [x] 使用原生`RabbitMQ` 库构建生产者与消费者模型
- [x] 整合 `Springboot` 实现 `RabbitMQ` 生产者与消费者模型
# 目录
内容会比较多,小伙伴们可以根据目录进行按需查阅。
[TOC]
# 一、需求分析

**业务流程如下:**
1、管理员进入管理界面点击 “页面发布”,前端请求 `cms` 页面发布接口。
2、cms 页面发布接口执行页面静态化,并将静态化页面(`html`文件)存储至`GridFS`中。
3、静态化成功后,向消息队列发送页面发布的消息。页面发布的最终目标是将页面发布到**服务器**。通过消息队列将页面发布的消息发送给各个服务器。
4、消息队列负责将消息发送给各各服务器上部署的 **Cms Client (Cms客户端)**。在服务器上部署 **Cms Client(Cms客户端)**,客户端接收消息队列的通知。
5、每个接收到页面发布消息的 `Cms Client` 从 `GridFS` 获取 `Html` 页面文件,并将 `Html` 文件存储在本地服务器。`CmsClient` 根据页面发布消息的内容请求 `GridFS` 获取页面文件,存储在本地服务器 。
# 二、初识RabbitMQ
要实现上边页面发布的功能,有一个重要的环节就是由消息队列将页面发布的消息通知给各各服务器。
本节的教学目标是对MQ的研究:
1、理解MQ的应用场景
2、理解MQ常用的工作模式
## 0x01 简单的介绍
### RabbitMQ 简介
MQ全称为 Message Queue,即消息队列, `RabbitMQ`是由 `erlang` 语言开发,基于**AMQP(Advanced Message Queue 高级消息队列协议)**协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
RabbitMQ官方地址:http://www.rabbitmq.com/
开发中消息队列通常有如下应用场景:
- **任务异步处理**
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
- **应用程序解耦合**
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
- **市场上还有哪些消息队列?**
ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。
- **为什么使用RabbitMQ呢?**
1、使得简单,功能强大。
2、基于 `AMQP` 协议。
3、社区活跃,文档完善。
4、高并发性能好,这主要得益于 `Erlang` 语言。
5、Spring Boot 默认已集成 `RabbitMQ`
### 其它相关知识
AMQP是什么 ?
`AMQP` 是一套公开的消息队列协议,最早在2003年被提出,它旨在从协议层定义消息通信数据的标准格式,为的就是解决 `MQ` 市场上协议不统一的问题。`RabbitMQ` 就是遵循 `AMQP` 标准协议开发的MQ服务。
JMS是什么 ?
`JMS`是 `java` 提供的一套消息服务API标准,其目的是为所有的 `java` 应用程序提供统一的消息通信的标准,类似 `java` 的 `jdbc`,只要遵循 `jms` 标准的应用程序之间都可以进行消息通信。它和 `AMQP` 有什么 不同,`jms` 是java语言专属的消息服务标准,它是在api层定义标准,并且只能用于 `java` 应用;而 `AMQP` 是在协议层定义的标准,是跨语言的 。
## 0x02 快速入门
### RabbitMQ 的工作原理
下图是 `RabbitMQ` 的基本结构

组成部分说明如下:
- **Broker**:消息队列服务进程,此进程包括两个部分:`Exchange` 和 `Queue`。
- **Exchange**:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过滤。
- **Queue**:消息队列,储存消息的队列,消息到达队列并转发给指定的消费方。
- **Producer**:消息生产者,即生产方客户端,生产方客户端将消息发送到 `MQ` 。
- **Consumer**:消息消费者,即消费方客户端,接收 `MQ` 转发的消息。
消息 **发布** 与 **接收** 流程:
-----发送消息-----
1、生产者和 `Broker` 建立TCP连接。
2、生产者和 `Broker` 建立通道。
3、生产者通过通道消息发送给 `Broker` ,由 `Exchange` 将消息进行转发。
4、`Exchange`将消息转发到指定的 `Queue`(队列)
----接收消息-----
1、消费者和 `Broker` 建立TCP连接
2、消费者和 `Broker` 建立通道
3、消费者监听指定的 `Queue`(队列)
4、当有消息到达 `Queue` 时 `Broker` 默认将消息推送给消费者。
5、消费者接收到消息。
### 安装 RabbitMQ
#### 1、下载并安装
RabbitMQ由 `Erlang` 语言开发,`Erlang` 语言用于并发及分布式系统的开发,在电信领域应用广泛,OTP(OpenTelecom Platform)作为 `Erlang` 语言的一部分,包含了很多基于 `Erlang` 开发的中间件及工具库,安装 `RabbitMQ` 需要安装 Erlang/OTP,并保持版本匹配,如下图:

本项目使用 `Erlang/OTP 20.3` 版本和 `RabbitMQ3.7.3` 版本。
官网 `RabbitMQ` 的下载地址:http://www.rabbitmq.com/download.html
##### 下载erlang
地址如下:http://erlang.org/download/otp_win64_20.3.exe
erlang安装完成需要配置erlang环境变量: ERLANG_HOME=D:\Program Files\erl9.3 在path中添
加%ERLANG_HOME%\bin;
##### 安装RabbitMQ
下载地址 https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.3
这里要注意 rabbitMQ的安装路径
#### 2、启动
安装成功后会自动创建RabbitMQ服务并且启动。
1. 在系统path变量中添加 `rabbitMQ` 的环境变量:`D:\[rabbitMQ的安装路径]\sbin;`
2. 添加环境变量后,按下 `Win + X` 以管理员身份运行powershell 或者 cmd
3. 运行 `rabbitmq-plugins.bat enable rabbitmq_management ` 命令

4. 运行 `rabbitmq-service.bat stop` 和 `rabbitmq-service.bat start` 重启 rabbitMQ
5. 启动成功,访问 http://localhost:15672 登录Rabbit MQ,初始账号密码为 guest/guest
#### 3、注意事项
1、安装 `erlang` 和 `rabbitMQ` 以管理员身份运行。
2、当卸载重新安装时会出现 `RabbitMQ` 服务注册失败,此时需要进入注册表清理 `erlang`
搜索RabbitMQ、ErlSrv,将对应的项全部删除。
### Hello World

以下过程我们参考官方教程(http://www.rabbitmq.com/getstarted.html)测试hello world:
#### 1、搭建环境
##### java client
生产者和消费者都属于客户端,rabbitMQ的java客户端如下:

我们先用 `rabbitMQ` 官方提供的 `java client` 测试,目的是对 `RabbitMQ` 的交互过程有个清晰的认识。
参考 :https://github.com/rabbitmq/rabbitmq-java-client/
##### 创建maven工程
创建生产者工程和消费者工程,分别加入`RabbitMQ java client`的依赖。
test-rabbitmq-producer:生产者工程
test-rabbitmq-consumer:消费者工程
**依赖配置如下:**
```xml
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.0.3</version><!--此版本与spring boot 1.5.9版本匹配-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
```

#### 2、生产者
在生产者工程下的单元测试内创建测试类如下
```java
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer01 {
//队列名称
private static final String QUEUE = "helloworld2";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = null;
Channel channel = null;
try {
//构建连接工厂,并设置一些基本的链接信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//rabbitMQ默认的虚拟机名称为“/”,虚拟机相当于一个独立的mq服务
factory.setVirtualHost("/");
//创建与RabbitMQ服务的TCP连接
connection = factory.newConnection();
//创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列,将自动创建
* String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
*
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
*/
channel.queueDeclare(QUEUE,true,false,false,null);
String message = "hello world 小明" + System.currentTimeMillis();
/**
* 消息发布方法
消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
* 这里没有指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显示绑定或解除绑定
* 默认的交换机,routingKey等于队列名称
*/
channel.basicPublish("",QUEUE,null,message.getBytes());
System.out.println("Send Message is: ' " + message + " '");
}catch ( Exception ex){
ex.printStackTrace();
}finally {
//先关闭通道,再关闭连接
if(channel != null){
channel.close();
}
if(channel != null){
connection.close();
}
}
}
}
```
运行生产者后,`RabbitMQ` 后台会接收到一条等待消费的消息

#### 3、消费者
```java
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer01 {
private static final String QUEUE = "helloworld2";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//rabbitMQ默认的虚拟机名称为“/”,虚拟机相当于一个独立的mq服务
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明队列
/** 参数:String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments
* 参数明细:
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭,队列则自动删除,可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive共同为true,就可以实现临时队列
* 5、argmuacnts,可以设置一个队列扩展参数,比如:可设置存活的时间
*/
channel.queueDeclare(QUEUE, true, false, false, null);
//定义消费方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
/**
* 消费者接收消息调用此方法
* @param consumerTag 消费者的标签,在channel.basicConsume()去指定
* @param envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志
(收到消息失败后是否需要重新发送)
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
1、发送端操作流程
1)创建连接
2)创建通道
3)声明队列
4)发送消息
2、接收端
1)创建连接
2)创建通道
3)声明队列
4)监听队列
5)接收消息
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
//路由key
String routingKey = envelope.getRoutingKey();
//消息id
long deliveryTag = envelope.getDeliveryTag();
//消息内容
String msg = new String(body, "utf-8");
System.out.println("receive message.." + msg);
}
};
/**
* 监听队列String queue, boolean autoAck,Consumer callback
* 参数明细
* 1、queue 队列名称
* 2、autoAck 是否自动回复,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动回复
* 3、callback 消费消息的方法,消费者接收到消息后调用此方法
*/
channel.basicConsume(QUEUE, true, defaultConsumer);
}
}
```
#### 4、消息接收测试

## 0x03 工作模式
`RabbitMQ` 有以下几种工作模式 :
1、Work queues,工作队列模式
2、Publish/Subscribe,发布订阅模式
3、Routing,路由模式
4、Topics,通配符模式
5、Header,header模式
6、RPC ,rpc模式
### Work queues 模式
`work queues` 工作模式,与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息。
应用场景:对于 **任务过重** 或 **任务较多** 情况使用工作队列可以提高任务处理的速度 。
**测试:**
1、使用入门程序,启动多个消费者。
2、生产者发送多个消息。
**测试结果:**

1、一条消息只会被一个消费者接收;
2、`rabbit` 采用 **轮询** 的方式将消息是平均发送给消费者的;
3、消费者在处理完某条消息后,才会收到下一条消息。
### Publish/Subscribe 模式
#### 1、发布订阅模式

1、每个消费者监听自己的队列。
2、生产者将消息发给 `broker`,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息 。
#### 2、代码
案例:
用户通知,当用户充值成功或转账完成系统通知用户,通知方式有短信、邮件多种方法 。
##### 生产者
声明 `Exchange_fanout_inform` 交换机。
声明两个队列并且绑定到此交换机,绑定时不需要指定 `routingkey`
发送消息时不需要指定 `routingkey`
```java
package rabbitmq;
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;
public class Producer02_publish {
//队列名称
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_FANOUT_INFORM = "exchange_fanout_inform";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = null;
Channel channel = null;
try {
//构建连接工厂,并设置一些基本的链接信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//rabbitMQ默认的虚拟机名称为“/”,虚拟机相当于一个独立的mq服务
factory.setVirtualHost("/");
//创建与RabbitMQ服务的TCP连接
connection = factory.newConnection();
//创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
channel = connection.createChannel();
/**
* 声明交换机
* 1、交换机名称
* 2、交换机类型:fanout、topic、direct、headers
*/
channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM, BuiltinExchangeType.FANOUT);
/**
* 声明队列,如果Rabbit中没有此队列,将自动创建
* String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
*
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
/**
* 将交换机和队列进行绑定
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_FANOUT_INFORM,"");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_FANOUT_INFORM,"");
//发布消息
for (int i = 0; i < 5 ; i++) {
String message = "inform to user " + i;
/**
* 消息发布方法
消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
* 这里没有指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显示绑定或解除绑定
* 默认的交换机,routingKey等于队列名称
*/
channel.basicPublish(EXCHANGE_FANOUT_INFORM,"",null,message.getBytes());
System.out.println("Send Message is: " + message);
}
}catch ( Exception ex){
ex.printStackTrace();
}finally {
//关闭通道和连接
if(channel != null){
channel.close();
}
if(channel != null){
connection.close();
}
}
}
}
```
##### 邮件消费者
```java
package rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer02_subscribe_email {
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String EXCHANGE_FANOUT_INFORM = "exchange_fanout_inform";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//rabbitMQ默认的虚拟机名称为“/”,虚拟机相当于一个独立的mq服务
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明队列
/** 参数:String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments
* 参数明细:
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭,队列则自动删除,可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive共同为true,就可以实现临时队列
* 5、argmuacnts,可以设置一个队列扩展参数,比如:可设置存活的时间
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null);
/**
* 声明交换机
*/
channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM,BuiltinExchangeType.FANOUT);
/**
* 绑定交换机
*/
channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_FANOUT_INFORM,"");
//定义消费方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
//路由key
String routingKey = envelope.getRoutingKey();
//消息id
long deliveryTag = envelope.getDeliveryTag();
//消息内容
String msg = new String(body, "utf-8");
System.out.println("receive message.." + msg);
}
};
/**
* 监听队列String queue, boolean autoAck,Consumer callback
* 参数明细
* 1、queue 队列名称
* 2、autoAck 是否自动回复,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动回复
* 3、callback 消费消息的方法,消费者接收到消息后调用此方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL, true, defaultConsumer);
}
}
```
##### 短信消费者
参考上边的邮件发送消费者代码,修改队列名称即可
#### 3、测试

#### 4、思考
1. **publish/subscribe与work queues有什么区别。**
**区别:**
1)`work queues` 不用定义交换机,而 `publish/subscribe` 需要定义交换机。
2)`publish/subscribe` 的生产方是面向交换机发送消息,`work queues` 的生产方是面向队列
发送消息(底层使用默认交换机)。
3)`publish/subscribe` 需要设置队列和交换机的绑定,`work queues` 不需要设置,实质上`work queues` 会将队列绑定到默认的交换机 。
**相同点:**
所以两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息。
2. **实质工作用什么 publish/subscribe 还是 work queues**
建议使用 `publish/subscribe`,发布订阅模式比工作队列模式更强大,并且发布订阅模式可以指定自己专用的交换机。
### Routing 模式
#### 1、路由模式

1、每个消费者监听自己的队列,并且设置 `routingkey`。
2、生产者将消息发给交换机,由交换机根据 `routingkey` 来转发消息到指定的队列。
#### 2、代码
##### 生产者
路由模式的生产者代码基于 **发布订阅模式** 的代码,在这基础上增加以下几点
- 增加 `routing key` 以及 修改交换机的模式
- 为每个队列绑定 `routing key`
- 在下面的代码当中,我为 `CMS` 和 `EMAIL` 的队列分别绑定了一个单独的 `routing key`,再绑定了一个 共有的名为 `ALL` 的key,以便测试 `routing` 模式的特性
具体代码如下:
```java
package rabbitmq;
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;
public class Producer03_routing {
//队列名称
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
private static final String ROUTINGKEY_INFORM_EMAIL = "inform_email";
private static final String ROUTINGKEY_INFORM_SMS = "inform_sms";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = null;
Channel channel = null;
try {
//构建连接工厂,并设置一些基本的链接信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//rabbitMQ默认的虚拟机名称为“/”,虚拟机相当于一个独立的mq服务
factory.setVirtualHost("/");
//创建与RabbitMQ服务的TCP连接
connection = factory.newConnection();
//创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
channel = connection.createChannel();
/**
* 声明交换机
* 1、交换机名称
* 2、交换机类型:fanout、topic、direct、headers
*/
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
/**
* 声明队列,如果Rabbit中没有此队列,将自动创建
* String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
*
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
/**
* 将交换机和队列进行绑定
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_INFORM_SMS);
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_INFORM_EMAIL);
//两个队列都绑定一个ALL的KEY
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,"ALL");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,"ALL");
//发布消息到EMAIL
for (int i = 0; i < 5 ; i++) {
String message = "inform to email " + i;
/**
* 消息发布方法
消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
* 这里没有指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显示绑定或解除绑定
* 默认的交换机,routingKey等于队列名称
*/
channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_INFORM_EMAIL,null,message.getBytes());
System.out.println("Send Message is: " + message);
}
//发布消息SMS
for (int i = 0; i < 5 ; i++) {
String message = "inform to sms " + i;
channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_INFORM_SMS,null,message.getBytes());
System.out.println("Send Message is: " + message);
}
//发布消息ALL
for (int i = 0; i < 5 ; i++) {
String message = "inform to all user " + i;
channel.basicPublish(EXCHANGE_ROUTING_INFORM,"ALL",null,message.getBytes());
System.out.println("Send Message is: " + message);
}
}catch ( Exception ex){
ex.printStackTrace();
}finally {
//关闭通道和连接
if(channel != null){
channel.close();
}
if(channel != null){
connection.close();
}
}
}
}
```
单独运行生产者后,我们可以在 rabbitMQ 后台的 `EXCHANGES` 栏中看到,建立了4个队列对应 `routing key` 的通讯连接

##### 短信消费者
```java
package rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer03_routing_sms {
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
private static final String ROUTINGKEY_INFORM_SMS = "inform_sms";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//rabbitMQ默认的虚拟机名称为“/”,虚拟机相当于一个独立的mq服务
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明队列
/** 参数:String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments
* 参数明细:
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭,队列则自动删除,可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive共同为true,就可以实现临时队列
* 5、argmuacnts,可以设置一个队列扩展参数,比如:可设置存活的时间
*/
channel.queueDeclare(QUEUE_INFORM_SMS, true, false, false, null);
/**
* 声明交换机
*/
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM,BuiltinExchangeType.DIRECT);
/**
* 绑定交换机
*/
channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_ROUTING_INFORM,ROUTINGKEY_INFORM_SMS);
channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_ROUTING_INFORM,"ALL"); //绑定一个公有的key,用于接收公共的key
//定义消费方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
//路由key
String routingKey = envelope.getRoutingKey();
//消息id
long deliveryTag = envelope.getDeliveryTag();
//消息内容
String msg = new String(body, "utf-8");
System.out.println("receive message.." + msg);
}
};
/**
* 监听队列String queue, boolean autoAck,Consumer callback
* 参数明细
* 1、queue 队列名称
* 2、autoAck 是否自动回复,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动回复
* 3、callback 消费消息的方法,消费者接收到消息后调用此方法
*/
channel.basicConsume(QUEUE_INFORM_SMS, true, defaultConsumer);
}
}
```
这里要注意的一点是,在队列绑定交换机的代码中,除了绑定短信的key 我还单独绑定了一个 ALL 的key,用于接收全局的消息,代码块如下
```java
channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_ROUTING_INFORM,"ALL"); //绑定一个公有的key,用于接收公共的key
```
##### 邮件消费者
具体代码参考短信消费者的代码
#### 3、测试
预期结果:生产者分别发送5条消息到 **SMS消费者** 和 **EMAIL 消费者**,以及发送5条消息给**全部消费者**。
<img src="https://qnoss.codeyee.com/20200704_5/image15" style="zoom:67%;" />
#### 4、思考
1、Routing模式 和 Publish/subscibe有啥区别?
Routing模式要求队列在绑定交换机时要指定`routingkey`,消息会转发到符合 `routingkey` 的队列。
### Topics 模式
#### 4-1、通配符模式(Topics)

通配符路由模式:
1、每个消费者监听自己的队列,并且设置带 **通配符** 的 `routingkey`。
2、生产者将消息发给 `broker`,由交换机根据 `routingkey` 来转发消息到指定的队列。
#### 4-2、代码
根据用户的通知设置去通知用户,设置接收 `Email` 的用户只接收 `Email `,设置接收 `sms` 的用户只接收 `sms` ,设置两种通知类型都接收的则两种通知都有效
##### 生产者
声明交换机,指定topic类型:
核心代码
```java
/**
* 声明交换机
* param1:交换机名称
* param2:交换机类型 四种交换机类型:direct、fanout、topic、headers
*/
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
//Email通知
channel.basicPublish(EXCHANGE_TOPICS_INFORM, "inform.email", null, message.getBytes());
//sms通知
channel.basicPublish(EXCHANGE_TOPICS_INFORM, "inform.sms", null, message.getBytes());
//两种都通知
channel.basicPublish(EXCHANGE_TOPICS_INFORM, "inform.sms.email", null, message.getBytes());
```
全部代码如下:
```java
package rabbitmq;
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;
public class Producer04_topcis {
//队列名称
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
private static final String ROUTINGKEY_INFORM_EMAIL = "inform.#.email.#";
private static final String ROUTINGKEY_INFORM_SMS = "inform.#.sms.#";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = null;
Channel channel = null;
try {
//构建连接工厂,并设置一些基本的链接信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//rabbitMQ默认的虚拟机名称为“/”,虚拟机相当于一个独立的mq服务
factory.setVirtualHost("/");
//创建与RabbitMQ服务的TCP连接
connection = factory.newConnection();
//创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
channel = connection.createChannel();
/**
* 声明交换机
* 1、交换机名称
* 2、交换机类型:fanout、topic、direct、headers
*/
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
/**
* 声明队列,如果Rabbit中没有此队列,将自动创建
* String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
*
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
/**
* 将交换机和队列进行绑定
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_INFORM_SMS);
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_INFORM_EMAIL);
//发布消息到EMAIL
for (int i = 0; i < 5 ; i++) {
String message = "inform to email " + i;
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.email",null,message.getBytes());
System.out.println("Send Message is: " + message);
}
//发布消息SMS
for (int i = 0; i < 5 ; i++) {
String message = "inform to sms " + i;
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms",null,message.getBytes());
System.out.println("Send Message is: " + message);
}
//发布消息到cms和email
for (int i = 0; i < 5 ; i++) {
String message = "inform to sms and email " + i;
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms.email",null,message.getBytes());
System.out.println("Send Message is: " + message);
}
}catch ( Exception ex){
ex.printStackTrace();
}finally {
//关闭通道和连接
if(channel != null){
channel.close();
}
if(channel != null){
connection.close();
}
}
}
}
```
##### 消费者
队列绑定交换机指定通配符 `routing key`
统配符规则:中间以“.”分隔。符号#可以匹配多个词,符号*可以匹配一个词语。
核心代码如下,具体代码参考 `routing` 模式的消费者
```java
//声明队列
channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null);
channel.queueDeclare(QUEUE_INFORM_SMS, true, false, false, null);
//声明交换机
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
//绑定email通知队列
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,"inform.#.email.#");
//绑定sms通知队列
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,"inform.#.sms.#");
```
#### 4-3、测试
在生产者端分别向 inform.email、inform.sms、inform.sms.email 这三个通配符发送了消息

预期结果:ems和email消费者分别接收到 `sms` 和 `email` 信息和 `sms and email` 信息
**执行生产者**

**查看执行消费者端**
sms消费者

email消费者

#### 4-4、思考
1、本案例的需求使用 `routing` 模式是否能实现?
使用 `routing` 模式也可以实现本案例,共设置三个 `routing key` ,分别是 email、sms、all 这三个,email 队列绑定 `email` 和 `all` ,sms 队列绑定 sms 和 all,这样就可以实现上述的案例,但是实现过程比 `topics` 复杂。
Topics 模式更强大,它可以实现 `Routing` 、`publish/subscirbe` 模式的功能。
### Header 模式
`header` 模式与 `routing` 不同的地方在于,`header` 模式取消 `routing key`,使用 `header`中的 `key/value`(键值对)匹配
队列。
#### **案例**
根据用的通知设置去通知用户,设置接收 `Email` 的用户只接收`Email`,设置接收 `sms` 的用户只接收 `sms`,设置两种通知类型都接收的则两种通知都有效。
#### 代码
生产者
队列与交换机绑定的代码与之前不同,核心代码如下:
```java
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_type", "email");
Map<String, Object> headers_sms = new Hashtable<String, Object>();
headers_sms.put("inform_type", "sms");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_HEADERS_INFORM,"",headers_sms);
```
发布消息核心代码:
```java
String message = "email inform to user"+i;
Map<String,Object> headers = new Hashtable<String, Object>();
headers.put("inform_type", "email");//匹配email通知消费者绑定的header
//headers.put("inform_type", "sms");//匹配sms通知消费者绑定的header
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder();
properties.headers(headers);
//Email通知
channel.basicPublish(EXCHANGE_HEADERS_INFORM, "", properties.build(), message.getBytes());
```
#### 邮消费者
email 消费者核心代码
```java
channel.exchangeDeclare(EXCHANGE_HEADERS_INFORM, BuiltinExchangeType.HEADERS);
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_email", "email");
//交换机和队列绑定
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
//指定消费队列
channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer);
```
测试

### RPC模式

RPC即客户端远程调用服务端的方法 ,使用 `MQ` 可以实现 `RPC` 的异步调用,基于 `Direct` 交换机实现,流程如下:
1、客户端即是生产者就是消费者,向 `RPC` 请求队列发送 `RPC` 调用消息,同时监听 `RPC` 响应队列。
2、服务端监听 `RPC` 请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果
3、服务端将 `RPC` 方法 的结果发送到 `RPC` 响应队列
4、客户端(**RPC调用方**)监听`RPC`响应队列,接收到 `RPC` 调用结果。
## 0x04 Spring Boot整合RibbitMQ
### 环境搭建
我们选择基于Spring-Rabbit去操作`RabbitMQ`
源代码地址:https://github.com/spring-projects/spring-amqp
在我们之前创建得生产者模块中进行构建,添加如下依赖,注释掉之前原生的 `rabbitmq` 依赖
```xml
<dependencies>
<!--原生rabbitMQ-->
<!--<dependency>-->
<!--<groupId>com.rabbitmq</groupId>-->
<!--<artifactId>amqp-client</artifactId>-->
<!--<version>4.0.3</version><!–此版本与spring boot 1.5.9版本匹配–>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!--Springboot整合rabbitMQ启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
```
### 配置
1、配置application.yml
配置连接 `rabbitmq` 的参数
```yml
server:
port: 44000
spring:
application:
name: test-rabbitmq-producer
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtualHost: /
```
创建 Springboot启动程序
```java
package com.xuecheng.test.rabbitmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestRabbitmqApplication {
public static void main(String[] args) {
SpringApplication.run(TestRabbitmqApplication.class,args);
}
}
```
### 生产端
构建 `RabbitmqConfig` ,用于配置交换机以及绑定队列
```java
package com.xuecheng.test.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitmqConfig {
public static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
public static final String QUEUE_INFORM_SMS = "queue_inform_sms";
public static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
/**
* 交换机配置
* ExchangeBuilder提供了fanout、direct、topic、header交换机类型的配置
*/
@Bean(EXCHANGE_TOPICS_INFORM)
public Exchange EXCHANGE_TOPICS_INFORM(){
//DURABLE(true) 持久化,消息队列重启后交换机仍然存在
return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(true).build();
}
/**
* 声明队列
* @return
*/
@Bean(QUEUE_INFORM_SMS)
public Queue QUEUE_INFORM_SMS(){
Queue queue = new Queue(QUEUE_INFORM_SMS);
return queue;
}
@Bean(QUEUE_INFORM_EMAIL)
public Queue QUEUE_INFORM_EMAIL(){
Queue queue = new Queue(QUEUE_INFORM_EMAIL);
return queue;
}
/**
* 绑定队列到交换机
* @param queue 指定队列
* @param exchange 指定交换机
* @return
*/
@Bean
public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue,
@Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("inform.#.sms.#").noargs();
}
@Bean
public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue,
@Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("inform.#.email.#").noargs();
}
}
```
运行测试,成功生成5条消息到rabbitmq

> 运行生产端前删除原有的交换机
### 消费端
准备工作
1. 配置 **4.1** 给出的依赖包
2. 复制 生产者端的 `RabbitmqConfig.java`
3. 创建消费端 `Springboot` 启动程序
目录结构如下

配置 `ReceiveHandler.java`,监听 rabbitMQ 的消息
```java
package com.xuecheng.test.rabbitmq.mq;
import com.xuecheng.test.rabbitmq.config.RabbitmqConfig;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import com.rabbitmq.client.Channel;
import org.springframework.stereotype.Component;
@Component
public class ReceiveHandler {
//监听email队列
@RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL})
public void receive_email(String msg, Message message, Channel channel){
System.out.println("receive: "+msg);
}
//监听sms队列
@RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_SMS})
public void receive_sms(String msg, Message message,Channel channel){
System.out.println("receive: "+msg);
}
}
```
启动消费者测试,可以看到刚才我们运行生产者所生产的消息,如下图

## 进度复盘
从 **3/16** 开始 至今日 **3/27**,共12天时间,完成进度至day05,由于不能全脱产(还有其他计划内的事情需要完成)的形式进行学习该项目的知识点,每天能分配的时间不多,所以进度有点缓慢,也不及自己的预期,尽可能每周能完成 3~4 day 的进度。
# 😁 认识作者
作者:👦 LCyee ,一个向往体面生活的代码🐕
自建博客:[https://www.codeyee.com](https://www.codeyee.com)
> 记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。
CSDN 博客:[https://blog.csdn.net/codeyee](https://blog.csdn.net/codeyee)
> 记录和分享一些开发过程中遇到的问题以及解决的思路。
欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

![微服务[学成在线] day05:消息中间件 RabbitMQ](https://qnoss.codeyee.com/TIM%E5%9B%BE%E7%89%8720200721182350_1595327150222.jpg)
微服务[学成在线] day05:消息中间件 RabbitMQ