为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 day05
的内容
RabbitMQ
的基本应用场景RabbitMQ
库构建生产者与消费者模型Springboot
实现 RabbitMQ
生产者与消费者模型内容会比较多,小伙伴们可以根据目录进行按需查阅。
业务流程如下:
1、管理员进入管理界面点击 “页面发布”,前端请求 cms
页面发布接口。
2、cms 页面发布接口执行页面静态化,并将静态化页面(html
文件)存储至GridFS
中。
3、静态化成功后,向消息队列发送页面发布的消息。页面发布的最终目标是将页面发布到服务器。通过消息队列将页面发布的消息发送给各个服务器。
4、消息队列负责将消息发送给各各服务器上部署的 Cms Client (Cms客户端)。在服务器上部署 Cms Client(Cms客户端),客户端接收消息队列的通知。
5、每个接收到页面发布消息的 Cms Client
从 GridFS
获取 Html
页面文件,并将 Html
文件存储在本地服务器。CmsClient
根据页面发布消息的内容请求 GridFS
获取页面文件,存储在本地服务器 。
要实现上边页面发布的功能,有一个重要的环节就是由消息队列将页面发布的消息通知给各各服务器。
本节的教学目标是对MQ的研究:
1、理解MQ的应用场景
2、理解MQ常用的工作模式
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
是在协议层定义的标准,是跨语言的 。
下图是 RabbitMQ
的基本结构
组成部分说明如下:
Exchange
和 Queue
。MQ
。MQ
转发的消息。消息 发布 与 接收 流程:
-----发送消息-----
1、生产者和 Broker
建立TCP连接。
2、生产者和 Broker
建立通道。
3、生产者通过通道消息发送给 Broker
,由 Exchange
将消息进行转发。
4、Exchange
将消息转发到指定的 Queue
(队列)
----接收消息-----
1、消费者和 Broker
建立TCP连接
2、消费者和 Broker
建立通道
3、消费者监听指定的 Queue
(队列)
4、当有消息到达 Queue
时 Broker
默认将消息推送给消费者。
5、消费者接收到消息。
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
地址如下:http://erlang.org/download/otp_win64_20.3.exe
erlang安装完成需要配置erlang环境变量: ERLANG_HOME=D:\Program Files\erl9.3 在path中添
加%ERLANG_HOME%\bin;
下载地址 https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.3
这里要注意 rabbitMQ的安装路径
安装成功后会自动创建RabbitMQ服务并且启动。
在系统path变量中添加 rabbitMQ
的环境变量:D:\[rabbitMQ的安装路径]\sbin;
添加环境变量后,按下 Win + X
以管理员身份运行powershell 或者 cmd
运行 rabbitmq-plugins.bat enable rabbitmq_management
命令
运行 rabbitmq-service.bat stop
和 rabbitmq-service.bat start
重启 rabbitMQ
启动成功,访问 http://localhost:15672 登录Rabbit MQ,初始账号密码为 guest/guest
1、安装 erlang
和 rabbitMQ
以管理员身份运行。
2、当卸载重新安装时会出现 RabbitMQ
服务注册失败,此时需要进入注册表清理 erlang
搜索RabbitMQ、ErlSrv,将对应的项全部删除。
以下过程我们参考官方教程(http://www.rabbitmq.com/getstarted.html)测试hello world:
生产者和消费者都属于客户端,rabbitMQ的java客户端如下:
我们先用 rabbitMQ
官方提供的 java client
测试,目的是对 RabbitMQ
的交互过程有个清晰的认识。
参考 :https://github.com/rabbitmq/rabbitmq-java-client/
创建生产者工程和消费者工程,分别加入RabbitMQ java client
的依赖。
test-rabbitmq-producer:生产者工程
test-rabbitmq-consumer:消费者工程
依赖配置如下:
<dependencies>
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>4.0.3version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
dependencies>
在生产者工程下的单元测试内创建测试类如下
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 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
后台会接收到一条等待消费的消息
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 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);
}
}
RabbitMQ
有以下几种工作模式 :
1、Work queues,工作队列模式
2、Publish/Subscribe,发布订阅模式
3、Routing,路由模式
4、Topics,通配符模式
5、Header,header模式
6、RPC ,rpc模式
work queues
工作模式,与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息。
应用场景:对于 任务过重 或 任务较多 情况使用工作队列可以提高任务处理的速度 。
测试:
1、使用入门程序,启动多个消费者。
2、生产者发送多个消息。
测试结果:
1、一条消息只会被一个消费者接收;
2、rabbit
采用 轮询 的方式将消息是平均发送给消费者的;
3、消费者在处理完某条消息后,才会收到下一条消息。
1、每个消费者监听自己的队列。
2、生产者将消息发给 broker
,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息 。
案例:
用户通知,当用户充值成功或转账完成系统通知用户,通知方式有短信、邮件多种方法 。
声明 Exchange_fanout_inform
交换机。
声明两个队列并且绑定到此交换机,绑定时不需要指定 routingkey
发送消息时不需要指定 routingkey
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 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();
}
}
}
}
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 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);
}
}
参考上边的邮件发送消费者代码,修改队列名称即可
publish/subscribe与work queues有什么区别。
区别:
1)work queues
不用定义交换机,而 publish/subscribe
需要定义交换机。
2)publish/subscribe
的生产方是面向交换机发送消息,work queues
的生产方是面向队列
发送消息(底层使用默认交换机)。
3)publish/subscribe
需要设置队列和交换机的绑定,work queues
不需要设置,实质上work queues
会将队列绑定到默认的交换机 。
相同点:
所以两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息。
实质工作用什么 publish/subscribe 还是 work queues
建议使用 publish/subscribe
,发布订阅模式比工作队列模式更强大,并且发布订阅模式可以指定自己专用的交换机。
1、每个消费者监听自己的队列,并且设置 routingkey
。
2、生产者将消息发给交换机,由交换机根据 routingkey
来转发消息到指定的队列。
路由模式的生产者代码基于 发布订阅模式 的代码,在这基础上增加以下几点
routing key
以及 修改交换机的模式routing key
CMS
和 EMAIL
的队列分别绑定了一个单独的 routing key
,再绑定了一个 共有的名为 ALL
的key,以便测试 routing
模式的特性具体代码如下:
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 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
的通讯连接
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 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,用于接收全局的消息,代码块如下
channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_ROUTING_INFORM,"ALL"); //绑定一个公有的key,用于接收公共的key
具体代码参考短信消费者的代码
预期结果:生产者分别发送5条消息到 SMS消费者 和 EMAIL 消费者,以及发送5条消息给全部消费者。
1、Routing模式 和 Publish/subscibe有啥区别?
Routing模式要求队列在绑定交换机时要指定routingkey
,消息会转发到符合 routingkey
的队列。
通配符路由模式:
1、每个消费者监听自己的队列,并且设置带 通配符 的 routingkey
。
2、生产者将消息发给 broker
,由交换机根据 routingkey
来转发消息到指定的队列。
根据用户的通知设置去通知用户,设置接收 Email
的用户只接收 Email
,设置接收 sms
的用户只接收 sms
,设置两种通知类型都接收的则两种通知都有效
声明交换机,指定topic类型:
核心代码
/**
* 声明交换机
* 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());
全部代码如下:
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 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
模式的消费者
//声明队列
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.#");
在生产者端分别向 inform.email、inform.sms、inform.sms.email 这三个通配符发送了消息
预期结果:ems和email消费者分别接收到 sms
和 email
信息和 sms and email
信息
执行生产者
查看执行消费者端
sms消费者
email消费者
1、本案例的需求使用 routing
模式是否能实现?
使用 routing
模式也可以实现本案例,共设置三个 routing key
,分别是 email、sms、all 这三个,email 队列绑定 email
和 all
,sms 队列绑定 sms 和 all,这样就可以实现上述的案例,但是实现过程比 topics
复杂。
Topics 模式更强大,它可以实现 Routing
、publish/subscirbe
模式的功能。
header
模式与 routing
不同的地方在于,header
模式取消 routing key
,使用 header
中的 key/value
(键值对)匹配
队列。
根据用的通知设置去通知用户,设置接收 Email
的用户只接收Email
,设置接收 sms
的用户只接收 sms
,设置两种通知类型都接收的则两种通知都有效。
生产者
队列与交换机绑定的代码与之前不同,核心代码如下:
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);
发布消息核心代码:
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 消费者核心代码
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即客户端远程调用服务端的方法 ,使用 MQ
可以实现 RPC
的异步调用,基于 Direct
交换机实现,流程如下:
1、客户端即是生产者就是消费者,向 RPC
请求队列发送 RPC
调用消息,同时监听 RPC
响应队列。
2、服务端监听 RPC
请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果
3、服务端将 RPC
方法 的结果发送到 RPC
响应队列
4、客户端(RPC调用方)监听RPC
响应队列,接收到 RPC
调用结果。
我们选择基于Spring-Rabbit去操作RabbitMQ
源代码地址:https://github.com/spring-projects/spring-amqp
在我们之前创建得生产者模块中进行构建,添加如下依赖,注释掉之前原生的 rabbitmq
依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
dependencies>
1、配置application.yml
配置连接 rabbitmq
的参数
server:
port: 44000
spring:
application:
name: test-rabbitmq-producer
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtualHost: /
创建 Springboot启动程序
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
,用于配置交换机以及绑定队列
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
运行生产端前删除原有的交换机
准备工作
RabbitmqConfig.java
Springboot
启动程序目录结构如下
配置 ReceiveHandler.java
,监听 rabbitMQ 的消息
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
记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。
CSDN 博客:https://blog.csdn.net/codeyee
记录和分享一些开发过程中遇到的问题以及解决的思路。
欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。