1. 页面发布
1. 需求分析
业务流程如下:
- 管理员进入管理界面点击“页面发布”,前端请求cms页面发布接口
- cms页面发布接口执行页面静态化,并将静态化页面(html文件)存储至GridFS中
- 静态化成功后,向消息队列发送页面发布的消息。
- 消息队列负责将消息发送给各个服务器上部署的Cms Client(Cms客户端)。
- 每个接收到页面发布消息的Cms Client从GridFS获取Html页面文件,并将Html文件存储在本地服务器
2. RabbitMQ研究
消息队列将页面发布的消息通知给各个服务器 ,具体见第二部分
2. RabbitMQ研究
1. 介绍
1. RabbitMQ
MQ全称Message Queue,消息队列,RabbitMQ由erlang语言开发,基于AMQP(Advanced Message Queue,高级消息队列协议)协议实现的消息队列,是一种应用程序之间的通信方法,在分布式开发中应用广泛。
应用场景:1. 任务异步处理,提高应用程序的响应时间;2.应用程序解耦合
RabbitMQ的优势:
- 使用简单,功能强大
- 基于AMQP协议
- 社区活跃,文档完善
- 高并发性能好,得益于Erlang语言
- Spring Boot默认集成RabbitMQ
2. 其他相关知识
AMQP
AMQP是一套公开的消息队列协议,旨在从协议层定义消息通信数据的标准格式,为了解决MQ市场上协议不统一的问题。RabbitMQ就是遵循AMQP标准协议开发的MQ服务。
JMS
java提供的一套消息服务API标准,为了所有的java应用提供统一的消息通信的标准,类似jdbc,和AMQP的区别:jms是java语言专属的消息服务标准,api层定义标准,只能用于java应用;AMQP是在协议层定义的标准,跨语言。
3. 快速入门
1. RabbitMQ的工作原理
下图是RabbitMQ的基本结构
组成部分说明:
- Broker:消息队列服务进程,包括两部分Exchange和Queue
- Exchange: 消息队列交换机,按一定规则将消息路由转发到某个队列,对消息过滤。
- Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方
- Producer:消息生产者,即生产方客户端,生产方客户端将消息发送给MQ
- Consumer:消息消费者,即消费方客户端,接收MQ转发的消息
消息发布接收流程:
- 发送消息
- 生产者和Broker建立TCP连接
- 生产者和Broker建立channel通道
- 生产者通过通道消息发送给Broker,由Exchange将消息转发
- Exchange将消息转发到指定的Queue(队列)
- 接收消息
- 消费者和Broker建立TCP连接
- 消费者和Broker建立channel通道
- 消费者监听指定的Queue(队列)
- 当有消息到达Queue时Broker默认将消息 推送给消费者
- 消费者接收到消息
2. 下载安装
RabbitMQ由Erlang语言开发,Erlang语言用于并发及分布式系统的开发,在电信领域应用广泛,OTP(Open Telecom Platform)作为Erlang语言的一部分,包含了很多基于Erlang开发的中间件及工具库,安装RabbitMQ需要安装Erlang/OTP,并保持版本匹配
本项目使用Erlang/OTP 20.3版本和RabbitMQ3.7.3版本
1. 下载安装
- erlang的下载
erlang安装完成需要配置erlang环境变量: ERLANG_HOME=D:\Program Files\erl9.3 在path中添加%ERLANG_HOME%\bin;
- RabbitMQ 的安装
2. 启动
安装成功后会自动创建RabbitMQ服务并且启动
- 安装并运行服务 rabbitmq-service.bat install 安装服务 rabbitmq-service.bat stop 停止服务 rabbitmq-service.bat start 启动服务
- 安装管理插件 安装rabbitMQ的管理插件,方便在浏览器端管理RabbitMQ 管理员身份运行 rabbitmq-plugins.bat enable rabbitmq_management
- 启动成功 登录RabbitMQ 进入浏览器,输入:
http://localhost:15672
初始账号和密码:guest/guest
2. hello world
1. 搭建环境
- 创建maven工程 创建生产者工程和消费者工程,分别加入RabbitMQ java client的依赖
生产者和消费者都属于客户端,rabbitMQ的java客户端如下:
test-rabbitmq-producer 生产者工程
test-rabbitmq-consumer :消费者工程
<dependencies>
<dependency>
<!--此版本与spring boot 1.5.9版本匹配-->
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
2. 生产者
在生产者工程的test中创建测试类
public class Producer01 {
// 队列
private static final String QUEUE = "helloworld";
public static void main(String[] args) {
// 通过连接工厂创建新的连接和mq建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
connectionFactory.setVirtualHost("/");
// 建立新连接
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
// 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
channel = connection.createChannel();
// 声明队列
// 参数
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 队列名称
* 2. durable:是否持久化,如果持久化,mq重启后队列还在
* 3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
* 4. autoDelete:自动删除,队列不再使用时是否自动删除
* 5. arguments:参数,设置一个队列的扩展参数,如存活时间
*/
channel.queueDeclare(QUEUE,true,false,false,null);
// 发送消息
/**
* String exchange, String routingKey, BasicProperties props, byte[] body
* 1. exchange:交换机,如果不指定使用mq的默认交换机
* 2. routingKey:路由key,交换机根据路由key将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3. props:消息属性
* 4. body:消息内容
*/
String message = "hello world 黑马程序员";
channel.basicPublish("",QUEUE,null,message.getBytes());
System.out.println("send to mq..........");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 先关闭通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3. 消费者
消费者工程下的test创建测试类
public class Consumer01 {
// 队列
private static final String QUEUE = "helloworld";
public static void main(String[] args) throws IOException, TimeoutException {
// 通过连接工厂创建新的连接和mq建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
connectionFactory.setVirtualHost("/");
// 建立新连接
Connection connection = connectionFactory.newConnection();
// 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 监听队列
// 声明队列
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 队列名称
* 2. durable:是否持久化,如果持久化,mq重启后队列还在
* 3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
* 4. autoDelete:自动删除,队列不再使用时是否自动删除
* 5. arguments:参数,设置一个队列的扩展参数,如存活时间
*/
channel.queueDeclare(QUEUE,true,false,false,null);
// 监听队列
// 实现消费方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
// 接收到消息,此方法将被调用
/**
*
* @param consumerTag 消费者标签,标识消费者,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 交换机
String exchange = envelope.getExchange();
// 消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// 消息内容
String message = new String(body,"utf-8");
System.out.println("receive message: "+message);
}
};
// 参数:
/**
* String queue, boolean autoAck, Consumer callback
* 1. queue:队列名称
* 2. autoAck: 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true会自动回复mq,false需要编程实现回复
* 3. callback: 消费方法,当消费者接受到消息要执行的方法
*/
channel.basicConsume(QUEUE,true,defaultConsumer);
}
}
4. 总结
- 发送端操作流程
- 创建连接
- 创建通道
- 声明队列
- 发送消息
- 接收端
- 创建连接
- 创建通道
- 声明队列
- 监听队列
- 接收消息
- ack回复
4. 工作模式
有以下几种工作模式
- Work queues
- Publish/Subscribe
- Routing
- Topics
- Header
- RPC
1. Work queues
与入门程序相比,多了个消费端,两个消费端共同消费同一个队列中的消息
应用场景:对于任务过重或任务较多情况使用工作队列提高任务处理的速度
测试:启动多个消费者,生产者发送多个消息
结果:
- 一条消息只会被一个消费者接收
- rabbitMQ采用轮询的方式将消息平均发送给消费者
- 消费者在处理完某条消息后,才会收到下一条消息
2. Publish/subscribe
1. 工作模式
发布订阅模式:
- 每个消费者监听自己的队列
- 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将收到消息
2. 代码
案例:用户充值成功或转账完成系统通知用户,通知方式有短信和微信公众号等方法。
- 生产者
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) {
// 通过连接工厂创建新的连接和mq建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
connectionFactory.setVirtualHost("/");
// 建立新连接
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
// 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
channel = connection.createChannel();
// 声明队列
// 参数
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 队列名称
* 2. durable:是否持久化,如果持久化,mq重启后队列还在
* 3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
* 4. autoDelete:自动删除,队列不再使用时是否自动删除
* 5. arguments:参数,设置一个队列的扩展参数,如存活时间
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
// 声明交换机
/**
* 参数:
* String exchange 交换机的名称
* String type 交换机的类型
* 1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 对应的routing工作模式
* 3. topic: 对应的topics工作模式
* 4. headers:对应headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM, BuiltinExchangeType.FANOUT);
// 交换机和队列绑定
/**
* 参数 String queue, String exchange, String routingKey
* 1. queue 队列名称
* 2. exchange 交换机名称
* 3. routingKey 路由key,作用是交换机根据路由key的值将消息转发到指定的队列,发布订阅莫斯设为空字符串
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_FANOUT_INFORM,"");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_FANOUT_INFORM,"");
// 发送消息
/**
* String exchange, String routingKey, BasicProperties props, byte[] body
* 1. exchange:交换机,如果不指定使用mq的默认交换机
* 2. routingKey:路由key,交换机根据路由key将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3. props:消息属性
* 4. body:消息内容
*/
for (int i = 0; i < 5; i++) {
String message = "send inform message to user";
channel.basicPublish(EXCHANGE_FANOUT_INFORM,"",null,message.getBytes());
System.out.println("send to mq..........");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 先关闭通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 消费者,邮件消费者
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 {
// 通过连接工厂创建新的连接和mq建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
connectionFactory.setVirtualHost("/");
// 建立新连接
Connection connection = connectionFactory.newConnection();
// 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 监听队列
// 声明队列
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 队列名称
* 2. durable:是否持久化,如果持久化,mq重启后队列还在
* 3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
* 4. autoDelete:自动删除,队列不再使用时是否自动删除
* 5. arguments:参数,设置一个队列的扩展参数,如存活时间
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
// 监听队列
// 实现消费方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
// 接收到消息,此方法将被调用
/**
*
* @param consumerTag 消费者标签,标识消费者,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 交换机
String exchange = envelope.getExchange();
// 消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// 消息内容
String message = new String(body,"utf-8");
System.out.println("receive message: "+message);
}
};
// 参数:
/**
* String queue, boolean autoAck, Consumer callback
* 1. queue:队列名称
* 2. autoAck: 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true会自动回复mq,false需要编程实现回复
* 3. callback: 消费方法,当消费者接受到消息要执行的方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
}
}
- 短信发送消费者,同上面类似
3. 测试
生产者发送的消息,每条都转发到了各个队列,每个消费者都接收到消息
4. publish/subscribe 和 work queues的区别
区别:
- work queues不用定义交换机,而publish/subscribe需要定义交换机
- publish/subscribe的生产方是面向交换机发送消息,work queues的生产方是面向队列发送消息(底层使用默认交换机)
- publish/subscribe需要设置队列和交换机的绑定,work queues不需要设置,实质上work queues会将队列绑定到默认的交换机
相同点:
两者实现的发布/订阅的效果是一样的,多个消费端监听同一个队列不会重复消费消息
**建议使用 publish/subscribe,**发布订阅模式比工作队列模式更强大,并且发布订阅模式可以指定自己专用的交换机。
3. Routing
1. 工作模式
路由模式:
- 每个消费者监听自己的队列,并且设置routingkey
- 生产者将消息发给交换机,由交换机根据routingkey转发消息给指定队列
2. 代码
- 生产者
声明exchange_routing_inform交换机
声明两个队列并且绑定到此交换机,绑定时需要指定routingkey
发送消息时需要指定routingkey
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_EMAIL = "inform_email";
private static final String ROUTINGKEY_SMS = "inform_sms";
public static void main(String[] args) {
// 通过连接工厂创建新的连接和mq建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
connectionFactory.setVirtualHost("/");
// 建立新连接
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
// 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
channel = connection.createChannel();
// 声明队列
// 参数
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 队列名称
* 2. durable:是否持久化,如果持久化,mq重启后队列还在
* 3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
* 4. autoDelete:自动删除,队列不再使用时是否自动删除
* 5. arguments:参数,设置一个队列的扩展参数,如存活时间
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
// 声明交换机
/**
* 参数:
* String exchange 交换机的名称
* String type 交换机的类型
* 1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 对应的routing工作模式
* 3. topic: 对应的topics工作模式
* 4. headers:对应headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
// 交换机和队列绑定
/**
* 参数 String queue, String exchange, String routingKey
* 1. queue 队列名称
* 2. exchange 交换机名称
* 3. routingKey 路由key,作用是交换机根据路由key的值将消息转发到指定的队列,发布订阅莫斯设为空字符串
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,"inform");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,"inform");
// 发送消息
/**
* String exchange, String routingKey, BasicProperties props, byte[] body
* 1. exchange:交换机,如果不指定使用mq的默认交换机
* 2. routingKey:路由key,交换机根据路由key将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3. props:消息属性
* 4. body:消息内容
*/
/* for (int i = 0; i < 5; i++) {
String message = "send email inform message to user";
channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL,null,message.getBytes());
System.out.println("send to mq..........");
}
for (int i = 0; i < 5; i++) {
String message = "send sms inform message to user";
channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS,null,message.getBytes());
System.out.println("send to mq..........");
}*/
for (int i = 0; i < 5; i++) {
String message = "send inform message to user";
channel.basicPublish(EXCHANGE_ROUTING_INFORM,"inform",null,message.getBytes());
System.out.println("send to mq..........");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 先关闭通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 邮件发送消费者
public class Consumer03_routing_email {
// 队列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
private static final String ROUTINGKEY_EMAIL = "inform_email";
public static void main(String[] args) throws IOException, TimeoutException {
// 通过连接工厂创建新的连接和mq建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
connectionFactory.setVirtualHost("/");
// 建立新连接
Connection connection = connectionFactory.newConnection();
// 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 监听队列
// 声明队列
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 队列名称
* 2. durable:是否持久化,如果持久化,mq重启后队列还在
* 3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
* 4. autoDelete:自动删除,队列不再使用时是否自动删除
* 5. arguments:参数,设置一个队列的扩展参数,如存活时间
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
// 声明交换机
/**
* 参数:
* String exchange 交换机的名称
* String type 交换机的类型
* 1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 对应的routing工作模式
* 3. topic: 对应的topics工作模式
* 4. headers:对应headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
// 绑定队列
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
// 监听队列
// 实现消费方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
// 接收到消息,此方法将被调用
/**
*
* @param consumerTag 消费者标签,标识消费者,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 交换机
String exchange = envelope.getExchange();
// 消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// 消息内容
String message = new String(body,"utf-8");
System.out.println("receive message: "+message);
}
};
// 参数:
/**
* String queue, boolean autoAck, Consumer callback
* 1. queue:队列名称
* 2. autoAck: 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true会自动回复mq,false需要编程实现回复
* 3. callback: 消费方法,当消费者接受到消息要执行的方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
}
}
- 短信发送消费者,类似上面邮件的代码
3. 测试
打开RabbitMQ的管理界面,观察交换机绑定情况
有routingkey,根据routingkey转发消息到指定的队列
4. 思考
Routing模式和Publish/subscibe的区别: Routing模式要求队列在绑定交换机时要指定routingkey,消息会转发到符合routingkey的队列。
4. topics
1. 工作模式
路由模式:
- 每个消费者监听自己的队列,并且设置带通配符的routingkey
- 生产者将消息发给broker,由交换机根据routingkey转发消息到指定队列
2. 代码
案例:
根据用户的通知设置去通知用户,设置接收Email的用户只接收email,设置接收sms的用户只接收sms,设置两种都接收的则两种都有效。
- 生产者
声明交换机,指定topic类型
public class Producer04_topics {
// 队列
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_EMAIL = "inform.#.email.#";
private static final String ROUTINGKEY_SMS = "inform.#.sms.#";
public static void main(String[] args) {
// 通过连接工厂创建新的连接和mq建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
connectionFactory.setVirtualHost("/");
// 建立新连接
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
// 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
channel = connection.createChannel();
// 声明队列
// 参数
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 队列名称
* 2. durable:是否持久化,如果持久化,mq重启后队列还在
* 3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
* 4. autoDelete:自动删除,队列不再使用时是否自动删除
* 5. arguments:参数,设置一个队列的扩展参数,如存活时间
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
// 声明交换机
/**
* 参数:
* String exchange 交换机的名称
* String type 交换机的类型
* 1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 对应的routing工作模式
* 3. topic: 对应的topics工作模式
* 4. headers:对应headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
// 交换机和队列绑定
/**
* 参数 String queue, String exchange, String routingKey
* 1. queue 队列名称
* 2. exchange 交换机名称
* 3. routingKey 路由key,作用是交换机根据路由key的值将消息转发到指定的队列,发布订阅莫斯设为空字符串
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_SMS);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,"inform");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,"inform");
// 发送消息
/**
* String exchange, String routingKey, BasicProperties props, byte[] body
* 1. exchange:交换机,如果不指定使用mq的默认交换机
* 2. routingKey:路由key,交换机根据路由key将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3. props:消息属性
* 4. body:消息内容
*/
for (int i = 0; i < 5; i++) {
String message = "send email inform message to user";
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.email",null,message.getBytes());
System.out.println("send to mq..........");
}
/* for (int i = 0; i < 5; i++) {
String message = "send sms inform message to user";
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms",null,message.getBytes());
System.out.println("send to mq..........");
}*/
for (int i = 0; i < 5; i++) {
String message = "send sms and email inform message to user";
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms.email",null,message.getBytes());
System.out.println("send to mq..........");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 先关闭通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 消费端
队列绑定交换机指定通配符
通配符规则:
- 中间以
.
分隔 - 符号
#
可以匹配多个词,符号*
可以匹配一个词
public class Consumer04_topics_email {
// 队列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
private static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
public static void main(String[] args) throws IOException, TimeoutException {
// 通过连接工厂创建新的连接和mq建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 设置虚拟机,一个mq服务可以设置多个虚拟机,每个虚拟机相当于一个独立的mq
connectionFactory.setVirtualHost("/");
// 建立新连接
Connection connection = connectionFactory.newConnection();
// 创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 监听队列
// 声明队列
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 队列名称
* 2. durable:是否持久化,如果持久化,mq重启后队列还在
* 3. exclusive:是否独占连接,队列只允许在该链接中访问,如果连接关闭后,队列自动删除,可用于临时队列
* 4. autoDelete:自动删除,队列不再使用时是否自动删除
* 5. arguments:参数,设置一个队列的扩展参数,如存活时间
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
// 声明交换机
/**
* 参数:
* String exchange 交换机的名称
* String type 交换机的类型
* 1. fanout: 对应的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 对应的routing工作模式
* 3. topic: 对应的topics工作模式
* 4. headers:对应headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
// 绑定队列
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
// 监听队列
// 实现消费方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
// 接收到消息,此方法将被调用
/**
*
* @param consumerTag 消费者标签,标识消费者,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 交换机
String exchange = envelope.getExchange();
// 消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// 消息内容
String message = new String(body,"utf-8");
System.out.println("receive message: "+message);
}
};
// 参数:
/**
* String queue, boolean autoAck, Consumer callback
* 1. queue:队列名称
* 2. autoAck: 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为true会自动回复mq,false需要编程实现回复
* 3. callback: 消费方法,当消费者接受到消息要执行的方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
}
}
3. 测试
生产者发送若干条消息,交换机根据routekey通配符匹配并转发到指定的队列
4. 思考
需求可否由routing工作模式实现?
使用routing模式也可以实现,但是过程比topics复杂
topic模式更加强大,可以实现routing、publish/subscribe模式的功能
5. header模式
header模式取消routingkey,使用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());
- 消费者
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);
3. 测试
6. PRC
RPC即客户端远程调用服务端的方法 ,使用MQ可以实现RPC的异步调用,基于Direct交换机实现 ,流程:
- 客户端既是生产者也是消费者,向RPC请求队列发送RPC调用消息,同时监听RPC响应队列
- 服务端监听RPC请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果
- 服务端将RPC方法 的结果发送到RPC响应队列
- 客户端(RPC调用方)监听RPC响应队列,接收到RPC调用结果。
5. spring整合RabbitMQ
1. 搭建springboot环境
基于springboot操作RabbitMQ
<dependencies>
<!--<dependency>
<!–此版本与spring boot 1.5.9版本匹配–>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.0.3</version>
</dependency>-->
<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>
2. 配置
1. 配置application.yml
server:
port: 44000
spring:
application:
name: test-rabbitmq-producer
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtualHost: /
2. 定义RabbitConfig类
配置Exchange Queue以及绑定交换机
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";
public static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
public static final String ROUTINGKEY_SMS = "inform.#.sms.#";
// 声明交换机
@Bean(EXCHANGE_TOPICS_INFORM)
public Exchange EXCHANGE_TOPICS_INFORM(){
// durable 持久化
return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(false).build();
}
// 声明队列QUEUE_INFORM_EMAIL
@Bean(QUEUE_INFORM_EMAIL)
public Queue QUEUE_INFORM_EMAIL(){
return new Queue(QUEUE_INFORM_EMAIL);
}
// 声明队列QUEUE_INFORM_SMS
@Bean(QUEUE_INFORM_SMS)
public Queue QUEUE_INFORM_SMS(){
return new Queue(QUEUE_INFORM_SMS);
}
// 绑定email交换机和队列,指定routingkey
@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(ROUTINGKEY_EMAIL).noargs();
}
// 绑定sms交换机和队列,指定routingkey
@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(ROUTINGKEY_SMS).noargs();
}
}
3. 生产端
使用RabbitTemplate发送消息
@RunWith(SpringRunner.class)
@SpringBootTest
public class Producer05_topics_springboot {
@Autowired
private RabbitTemplate rabbitTemplate;
// 使用rabbittemplate发送消息
@Test
public void testSendEmail(){
String message = "send email message to user";
/**
* 参数
* 1. 交换机名称
* 2. routingkey
* 3. 消息内容
*/
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_TOPICS_INFORM,"inform.email",message);
}
}
4. 消费端
创建消费端工程,添加依赖,和生产端一样
使用@RabbitListener
注解监听队列
@Component
public class ReceiveHandler {
@RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL})
public void send_email(String msg, Message message, Channel channel){
System.out.println("receive message is: "+msg);
}
}