一、RabbitMQ介绍
MQ全称为Message Queue,即消息队列, RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message
Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开
发中应用非常广泛。RabbitMQ官方地址:http://www.rabbitmq.com/
开发中消息队列通常有如下应用场景:
1、任务异步处理。
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
2、应用程序解耦合
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
市场上还有哪些消息队列?
ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。
为什么使用RabbitMQ呢?
1、使得简单,功能强大。
2、基于AMQP协议。
3、社区活跃,文档完善。
4、高并发性能好,这主要得益于Erlang语言。
5、Spring Boot默认已集成RabbitMQ
二、 RabbitMQ 中的概念模型
消息模型:
所有 MQ 产品从模型抽象上来说都是一样的过程:消费者(consumer)订阅某个队列。生产者(producer)创建消息,然后发布到队列(queue)中,
最后将消息发送到监听的消费者。
RabbitMQ的基本概念
下图是RabbitMQ的基本结构:
组成部分说明如下:
- Broker :消息队列服务进程,此进程包括两个部分:Exchange和Queue。
- Exchange :消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。
- Queue :消息队列,存储消息的队列,消息到达队列并转发给指定的消费方,它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
- Producer :消息生产者,即生产方客户端,生产方客户端将消息发送到MQ。
- Consumer :消息消费者,即消费方客户端,接收MQ转发的消息。
- Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
- Binding:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
- Connection:网络连接,比如一个TCP连接。
- Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
- Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
Virtual Host理解如下图:
相关名词:
包括:ConnectionFactory(连接管理器)、Channel(信道)、Exchange(交换器)、Queue(队列)、RoutingKey(路由键)、BindingKey(绑定键)。
ConnectionFactory(连接管理器):应用程序与Rabbit之间建立连接的管理器,程序代码中使用;
Channel(信道):消息推送使用的通道;
Exchange(交换器):用于接受、分配消息;
Queue(队列):用于存储生产者的消息;
RoutingKey(路由键):用于把生成者的数据分配到交换器上;
BindingKey(绑定键):用于把交换器的消息绑定到队列上;
看到上面的解释,最难理解的路由键和绑定键了,那么他们具体怎么发挥作用的,请看下图:
消息发布接收流程:
-----发送消息-----
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(Open
Telecom Platform)作为Erlang语言的一部分,包含了很多基于Erlang开发的中间件及工具库,安装RabbitMQ需
要安装Erlang/OTP,并保持版本匹配,如下图:
RabbitMQ的下载地址:http://www.rabbitmq.com/download.html
1)下载erlang
地址如下:
http://erlang.org/download/otp_win64_20.3.exe,以管理员方式运行此文件,安装。
erlang安装完成需要配置erlang环境变量: ERLANG_HOME=D:\Program Files\erl9.3 在path中添
加%ERLANG_HOME%\bin;
2)安装RabbitMQhttps://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.3,以管理员方式运行此文件,安装
3)启动
- 安装成功后会自动创建RabbitMQ服务并且启动。
从开始菜单启动RabbitMQ,完成在开始菜单找到RabbitMQ的菜单:
RabbitMQ Service-install :安装服务
RabbitMQ Service-remove 删除服务
RabbitMQ Service-start 启动
RabbitMQ Service-stop 启动
2.如果没有开始菜单则进入安装目录下sbin目录手动启动:
1)安装并运行服务
rabbitmq-service.bat install 安装服务 rabbitmq-service.bat stop 停止服务 rabbitmq-service.bat start 启动服务
2)安装管理插件
安装rabbitMQ的管理插件,方便在浏览器端管理RabbitMQ
管理员身份运行 rabbitmq-plugins.bat enable rabbitmq_management
启动成功 登录RabbitMQ
进入浏览器,输入:http://localhost:15672
初始账号和密码:guest/guest
3) 注意事项:
1、安装erlang和rabbitMQ以管理员身份运行。
2、当卸载重新安装时会出现RabbitMQ服务注册失败,此时需要进入注册表清理erlang
搜索RabbitMQ、ErlSrv,将对应的项全部删除。
四、java操作队列
1、消息队列RabbitMQ的五种形式队列
1).点对点(简单)的队列
2).工作(公平性)队列模式
3.发布订阅模式
4.路由模式Routing
5.通配符模式Topics
2、简单队列
1)功能:一个生产者P发送消息到队列Q,一个消费者C接收
P表示为生产者 、C表示为消费者 红色表示队列。
点对点模式分析:
Maven依赖:
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.6.5</version>
</dependency>
</dependencies>
封装Connection:
/**
* 封装Connection
*/
public class MQConnectionUtils {
public static Connection getConnection(){
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务器地址
factory.setHost("localhost");
//设置端口号
factory.setPort(5672);
//设置用户名
factory.setUsername("guest");
//设置密码
factory.setPassword("guest");
//设置vhost
factory.setVirtualHost("/admin_yehui");
try {
//创建连接
return factory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return null;
}
}
生产者:
public class Producer {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//得到连接
Connection connection = MQConnectionUtils.getConnection();
//创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
Channel channel = connection.createChannel();
//声明队列 如果Rabbit中没有此队列将自动创建
/**
* 参数1:队列的名称
* 参数2:是否持久化
* 参数3:是否独占此链接
* 参数4:队列不在使用时是否自动删除
* 参数5:队列参数
*
*/
channel.queueDeclare(QUEUE_NAME, false,false, false, null);
String msg = "test_yehui_rabbitmq";
/**
* 发送消息
* 参数1: Exchange的名称,如果没有指定,则使用Default Exchange
* 参数2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* 参数3:消息包含的属性
* 参数4:消息体
* 这里没有指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显
* 示绑定或解除绑定认的交换机,routingKey等于队列名称
*/
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
System.out.println("消息发送体:"+msg);
channel.close();
connection.close();
}
}
消费者:
public class Consumer01 { private static final String QUEUE_NAME = "test_queue"; public static void main(String[] args) throws IOException { //得到连接 Connection connection = MQConnectionUtils.getConnection(); //创建一个通道 Channel channel = connection.createChannel(); //声明队列 /** * 参数1:队列的名称 * 参数2:是否持久化 * 参数3:是否独占此链接 * 参数4:队列不在使用时是否删除次队列 * 参数5:队列参数 */ channel.queueDeclare(QUEUE_NAME,false,false,false,null); //定义消费方法 DefaultConsumer consumer = 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 message = new String(body, "utf-8"); System.out.println("消费者消费:"+message); } }; //监听队列 /** * 参数1:队列名称 * 参数2: 设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置 * 为false则需要手动回复 * 参数3:消费消息的方法,消费者接收到消息后调用此方法 */ channel.basicConsume(QUEUE_NAME,true,consumer); } }
3、消息队列RabbitMQ应答模式
为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了。RabbitMQ就可以删除它了。 如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完全,然后交给另一个消费者去重新处理。这样,你就可以确认即使消费者偶尔挂掉也不会丢失任何消息了。 没有任何消息超时限制;只有当消费者挂掉时,RabbitMQ才会重新投递。即使处理一条消息会花费很长的时间。 消息应答是默认打开的。
我们通过显示的设置autoAsk=true关闭这种机制。现即自动应答开,一旦我们完成任务,消费者会自动发送应答。通知RabbitMQ消息已被处理,可以从内存删除。如果消费者因宕机或链接失败等原因没有发送ACK(不同于ActiveMQ,在RabbitMQ里,消息没有过期的概念),则RabbitMQ会将消息重新发送给其他监听在队列的下一个消费者。
4、工作队列
work queues与简单队列相比,多了一个消费端,两个消费端共同消费同一个队列中的消息。
应用场景:对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
P表示为生产者 、C表示为消费者 红色表示队列。
工作队列分析
均摊消费
发布订阅模式:
1、每个消费者监听自己的队列。
2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收
到消息
测试:
1、使用简单队列,启动多个消费者。
2、生产者发送多个消息。
结果:
1、一条消息只会被一个消费者接收;
2、rabbit采用轮询的方式将消息是平均发送给消费者的;
3、消费者在处理完某条消息后,才会收到下一条消息。
RabbitMQ的公平转发
目前消息转发机制是平均分配,这样就会出现俩个消费者,奇数的任务很耗时,偶数的任何工作量很小,造成的原因就是近当消息到达队列进行转发消息。并不在乎有多少任务消费者并未传递一个应答给RabbitMQ。仅仅盲目转发所有的奇数给一个消费者,偶数给另一个消费者。 为了解决这样的问题,我们可以使用basicQos方法,传递参数为prefetchCount= 1。这样告诉RabbitMQ不要在同一时间给一个消费者超过一条消息。 换句话说,只有在消费者空闲的时候会发送下一条信息。调度分发消息的方式,也就是告诉RabbitMQ每次只给消费者处理一条消息,也就是等待消费者处理完毕并自己对刚刚处理的消息进行确认之后,才发送下一条消息,防止消费者太过于忙碌,也防止它太过去清闲。 通过 设置channel.basicQos(1);
生产者
public class Producer {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//得到连接
Connection connection = MQConnectionUtils.getConnection();
//创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
Channel channel = connection.createChannel();
//声明队列 如果Rabbit中没有此队列将自动创建
/**
* 参数1:队列的名称
* 参数2:是否持久化
* 参数3:是否独占此链接
* 参数4:队列不在使用时是否自动删除
* 参数5:队列参数
*
*/
channel.queueDeclare(QUEUE_NAME, false,false, false, null);
/**
* 发送消息
* 参数1: Exchange的名称,如果没有指定,则使用Default Exchange
* 参数2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* 参数3:消息包含的属性
* 参数4:消息体
* 这里没有指定交换机,消息将发送给默认交换机,每个队列也会绑定那个默认的交换机,但是不能显
* 示绑定或解除绑定认的交换机,routingKey等于队列名称
*/
channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
for(int i=0;i<10;i++){
String msg = "test_yehui_rabbitmq"+i;
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
}
System.out.println("消息发送完毕");
channel.close();
connection.close();
}
}
消费者1:
public class Consumer01 {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException {
//得到连接
Connection connection = MQConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明队列
/**
* 参数1:队列的名称
* 参数2:是否持久化
* 参数3:是否独占此链接
* 参数4:队列不在使用时是否删除次队列
* 参数5:队列参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
//定义消费方法
DefaultConsumer consumer = 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 message = new String(body, "utf-8");
System.out.println("消费者消费:"+message);
try {
//睡眠1s
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 手动回执消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
//监听队列
/**
* 参数1:队列名称
* 参数2: 设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置
* 为false则需要手动回复
* 参数3:消费消息的方法,消费者接收到消息后调用此方法
*/
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
消费者2
public class Consumer02 {
//队列名称
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException {
//得到连接
Connection connection = MQConnectionUtils.getConnection();
//创建与Exchange的通道,每个连接可以创建多个通道,每个通道代表一个会话任务
Channel channel = connection.createChannel();
//声明队列 如果Rabbit中没有此队列将自动创建
/**
* 参数1:队列的名称
* 参数2:是否持久化
* 参数3:是否独占此链接
* 参数4:队列不在使用时是否自动删除
* 参数5:队列参数
*
*/
channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
channel.queueDeclare(QUEUE_NAME, false,false, false, null);
DefaultConsumer consumer = 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 message = new String(body, "utf-8");
System.out.println("消费者消费:"+message);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 手动回执消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
//监听队列
/**
* 参数1:队列名称
* 参数2: 设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置
* 为false则需要手动回复
* 参数3:消费消息的方法,消费者接收到消息后调用此方法
*/
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
结果;
消费者1比消费者2消费得少
5、RabbitMQ交换机的作用
生产者发送消息不会向传统方式直接将消息投递到队列中,而是先将消息投递到交换机中,在由交换机转发到具体的队列,队列在将消息以推送或者拉取方式给消费者进行消费,
这和我们之前学习Nginx有点类似。 交换机的作用根据具体的路由策略分发到不同的队列中,交换机有四种类型。
Direct exchange(直连交换机)是根据消息携带的路由键(routing key)将消息投递给对应队列的
Fanout exchange(扇型交换机)将消息路由给绑定到它身上的所有队列
Topic exchange(主题交换机)队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列
Headers exchange(头交换机)类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
p是生产者 X是交换机 C1 、C2 是消费者
6、发布/订阅模式Publish/Subscribe
基本概念:
这个可能是消息队列中最重要的队列了,其他的都是在它的基础上进行了扩展。
功能实现:一个生产者发送消息,多个消费者获取消息(同样的消息),包括一个生产者,一个交换机,多个队列,多个消费者。
思路解读(重点理解):
(1)一个生产者,多个消费者
(2)每一个消费者都有自己的一个队列
(3)生产者没有直接发消息到队列中,而是发送到交换机
(4)每个消费者的队列都绑定到交换机上
(5)消息通过交换机到达每个消费者的队列 该模式就是Fanout Exchange(扇型交换机)将消息路由给绑定到它身上的所有队列 以用户发邮件案例讲解
注意:交换机没有存储消息功能,如果消息发送到没有绑定消费队列的交换机,消息则丢失。
工作原理图:
生产者:
public class ProducerFanout {
//交换机
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
//得到连接
Connection connection = MQConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明交换机
/**
* 参数1:交换机名称
* 参数2:交换机类型
*/
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//发送消息
/**
* 参数1:交换机名称
* 参数2:路由key
* 参数3:消息属性参数
* 参数4:消息实体
*/
channel.basicPublish(EXCHANGE_NAME,"",null,"fanout_exchange_msg".getBytes());
channel.close();
connection.close();
}
}
邮件消费者
public class ConsumerEmailFanout {
//邮件队列
private static final String EMAIL_QUEUE = "email_queue";
//交换机
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException {
System.out.println("邮件消费者");
//得到连接
Connection connection = MQConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明一个队列
channel.queueDeclare(EMAIL_QUEUE,false,false,false,null);
//绑定交换机 // 4.消费者绑定交换机 参数1 队列 名称 参数2交换机名称 参数3 routingKey
channel.queueBind(EMAIL_QUEUE,EXCHANGE_NAME,"");
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
}
};
//消费者监听队列消息
channel.basicConsume(EMAIL_QUEUE,true,consumer);
}
}
短信消费者
public class ConsumerSMSFanout { //短信队列 private static final String SMS_QUEUE = "sms_queue"; //交换机 private static final String EXCHANGE_NAME = "fanout_exchange"; public static void main(String[] args) throws IOException { System.out.println("短信消费者"); //得到连接 Connection connection = MQConnectionUtils.getConnection(); //创建一个通道 Channel channel = connection.createChannel(); //声明一个队列 channel.queueDeclare(SMS_QUEUE,false,false,false,null); //绑定交换机 // 4.消费者绑定交换机 参数1 队列 名称 参数2交换机名称 参数3 routingKey channel.queueBind(SMS_QUEUE,EXCHANGE_NAME,""); DefaultConsumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("消费者获取生产者消息:" + msg); } }; //消费者监听队列消息 channel.basicConsume(SMS_QUEUE,true,consumer); } }
测试:先启动2个消费者,在启动生产者,结果2个消费者都能接受到消息
7、路由模式RoutingKey
路由模式:
1、每个消费者监听自己的队列,并且设置routingkey。
2、生产者发送消息到交换机并指定一个路由key,消费者队列绑定到交换机时要制定路由key(key匹配就能接受消息,key不匹配就不能接受消息)
生产者:
public class ProducerDirect {
//交换机名称
private static final String EXCHANGE_NAME = "direct_exchange";
//邮件队列名称
private static final String EMAIL_QUEUE = "email_direct_queue";
//短信队列名称
private static final String SMS_QUEUE = "sms_direct_queue";
//邮件routingkey
private static final String EMAIL_ROUTING_KEY = "email_routing_key";
//短信routingkey
private static final String SMS_ROUTING_KEY = "sms_routing_key";
public static void main(String[] args) throws IOException, TimeoutException {
//获得连接
Connection connection = MQConnectionUtils.getConnection();
//开启通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(EMAIL_QUEUE, false, false, false, null);
channel.queueDeclare(SMS_QUEUE, false, false, false, null);
//声明交换机
//3.绑定的交换机 参数1交互机名称 参数2 exchange类型
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//发送消息
channel.basicPublish(EXCHANGE_NAME, EMAIL_ROUTING_KEY, null, "email消息".getBytes());
channel.basicPublish(EXCHANGE_NAME, SMS_ROUTING_KEY, null, "sms消息".getBytes());
channel.close();
connection.close();
}
}
邮件消费者
public class ProducerDirect {
//交换机名称
private static final String EXCHANGE_NAME = "direct_exchange";
//邮件队列名称
private static final String EMAIL_QUEUE = "email_direct_queue";
//短信队列名称
private static final String SMS_QUEUE = "sms_direct_queue";
//邮件routingkey
private static final String EMAIL_ROUTING_KEY = "email_routing_key";
//短信routingkey
private static final String SMS_ROUTING_KEY = "sms_routing_key";
public static void main(String[] args) throws IOException, TimeoutException {
//获得连接
Connection connection = MQConnectionUtils.getConnection();
//开启通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(EMAIL_QUEUE, false, false, false, null);
channel.queueDeclare(SMS_QUEUE, false, false, false, null);
//声明交换机
//3.绑定的交换机 参数1交互机名称 参数2 exchange类型
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//发送消息
channel.basicPublish(EXCHANGE_NAME, EMAIL_ROUTING_KEY, null, "email消息".getBytes());
channel.basicPublish(EXCHANGE_NAME, SMS_ROUTING_KEY, null, "sms消息".getBytes());
channel.close();
connection.close();
}
}
短信消费者
public class ConsumerSMSDirect {
//交换机名称
private static final String EXCHANGE_NAME = "direct_exchange";
//短信队列名称
private static final String SMS_QUEUE = "sms_direct_queue";
//短信routingkey
private static final String SMS_ROUTING_KEY = "sms_routing_key";
public static void main(String[] args) throws IOException {
System.out.println("短信消费者");
//获得连接
Connection connection = MQConnectionUtils.getConnection();
//开启通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(SMS_QUEUE,false,false,false,null);
//交换机和队列进行绑定
/**
* 参数1:队列的名称
* 参数2:交换机的名称
* 参数3:路由key
*/
channel.queueBind(SMS_QUEUE,EXCHANGE_NAME,SMS_ROUTING_KEY);
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body,"utf-8");
System.out.println("接收到的消息:"+str);
}
};
channel.basicConsume(SMS_QUEUE,true,consumer);
}
}
启动测试,结果:邮件消费者只能接受邮件消息,短信消费者只能接收短信消息
8、通配符模式Topics
路由模式:
1、每个消费者监听自己的队列,并且设置带统配符的routingkey。
2、生产者P发送消息到交换机X,type=topic,交换机根据绑定队列的routing key的值进行通配符匹配,由交换机根据routingkey来转发消息到指定的队列。
符号#:匹配一个或者多个词lazy.# 可以匹配lazy.irs或者lazy.irs.cor
符号*:只能匹配一个词lazy.* 可以匹配lazy.irs或者lazy.cor
生产者:
public class ProducerTopic {
//交换机名称
private static final String EXCHANGE_NAME = "topic_exchange";
//邮件队列名称
private static final String EMAIL_QUEUE = "email_topic_queue";
//短信队列名称
private static final String SMS_QUEUE = "sms_topic_queue";
//邮件routingkey
private static final String EMAIL_ROUTING_KEY = "email.log";
//短信routingkey
private static final String SMS_ROUTING_KEY = "sms.log";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接
Connection connection = MQConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(EMAIL_QUEUE,false,false,false,null);
channel.queueDeclare(SMS_QUEUE,false,false,false,null);
//声明交换机
/**
* 参数1:交换机名称
* 参数2:交换机类型
*/
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
//发送·消息·
/**
* 参数1:交换机的名称
* 参数2:路由key
* 参数3:消息属性
* 参数4:传递消息体
*/
channel.basicPublish(EXCHANGE_NAME,EMAIL_ROUTING_KEY,null,"邮件topic".getBytes());
channel.basicPublish(EXCHANGE_NAME,SMS_ROUTING_KEY,null,"短信topic".getBytes());
channel.close();
connection.close();
}
}
邮件消费者:
public class ConsumerEmailTopic { //交换机名称 private static final String EXCHANGE_NAME = "topic_exchange"; //邮件队列名称 private static final String EMAIL_QUEUE = "email_topic_queue"; public static void main(String[] args) throws IOException { System.out.println("邮件消费者"); //创建连接 Connection connection = MQConnectionUtils.getConnection(); //创建一个通道 Channel channel = connection.createChannel(); //声明队列 channel.queueDeclare(EMAIL_QUEUE,false,false,false,null); //声明交换机 /** * 参数1:交换机名称 * 参数2:交换机类型 */ channel.exchangeDeclare(EXCHANGE_NAME,"topic"); //绑定交换机 /** * 参数1:队列的名称 * 参数2:交换机的名称 * 参数3:路由key 通配符 */ channel.queueBind(EMAIL_QUEUE,EXCHANGE_NAME,"email.*"); DefaultConsumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String str = new String(body,"utf-8"); System.out.println("消费者接受到了消息:"+str); } }; channel.basicConsume(EMAIL_QUEUE,true,consumer); } }
短信消费者:
public class ConsumerSmsTopic {
//交换机名称
private static final String EXCHANGE_NAME = "topic_exchange";
//短信队列名称
private static final String SMS_QUEUE = "sms_topic_queue";
public static void main(String[] args) throws IOException {
System.out.println("短信消费者");
//创建连接
Connection connection = MQConnectionUtils.getConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(SMS_QUEUE,false,false,false,null);
//声明交换机
/**
* 参数1:交换机名称
* 参数2:交换机类型
*/
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
//绑定交换机
/**
* 参数1:队列的名称
* 参数2:交换机的名称
* 参数3:路由key 通配符
*/
channel.queueBind(SMS_QUEUE,EXCHANGE_NAME,"sms.*");
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String str = new String(body,"utf-8");
System.out.println("消费者接受到了消息:"+str);
}
};
channel.basicConsume(SMS_QUEUE,true,consumer);
}
}
9、SpringBoot整合RabbitMQ
生产者:
maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- fastjson 依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
yml文件
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_yehui
定义RabbitConfig类,配置Exchange、Queue、及绑定交换机。
案例是用的是fanout交换机类型
@Configuration
public class RabbitMQConfig {
// 邮件队列
public static String FANOUT_EMAIL_QUEUE = "fanout_eamil_queue";
// 短信队列
public static String FANOUT_SMS_QUEUE = "fanout_sms_queue";
//交换机
public static String EXCHANGE_NAME = "fanoutExchange";
//定义邮件队列
@Bean("fanoutEamilQueue")
public Queue fanoutEamilQueue(){
return new Queue(FANOUT_EMAIL_QUEUE);
}
//定义短信队列
@Bean("fanoutSmsQueue")
public Queue fanoutSmsQueue(){
return new Queue(FANOUT_SMS_QUEUE);
}
//定义交换机
@Bean("fanoutExchange")
public FanoutExchange fanoutExchange(){
return new FanoutExchange(EXCHANGE_NAME);
}
//将邮件队列绑定交换机
@Bean("bindingEmailExchange")
public Binding bindingEmailExchange(@Qualifier("fanoutEamilQueue")Queue queue,
@Qualifier("fanoutExchange")FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
//将邮件队列绑定交换机
@Bean("bindingSmsExchange")
public Binding bindingSmsExchange(@Qualifier("fanoutSmsQueue")Queue queue,
@Qualifier("fanoutExchange")FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
}
生产者投递消息
/**
* 发送消息类
*/
@Component
public class FanoutProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String msg){
rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EMAIL_QUEUE,msg);
}
}
控制层调用代码
@RestController
public class RabbitController {
@Autowired
private FanoutProducer fanoutProducer;
@RequestMapping("/index")
public String index(){
fanoutProducer.send("邮件消息");
fanoutProducer.send("短信消息");
return "index";
}
}
消费者
maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- fastjson 依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
application.yml文件
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_yehui
server:
port: 8081
监听消息
/**
* 监听队列
*/
@Component
public class ReceiveHandler {
/**
* 邮箱
* @param msg
*/
@RabbitListener(queues={"fanout_eamil_queue"})
public void receive_email(String msg){
System.out.println("邮箱消费者获取生产者消息msg:" + msg);
}
/**
* 短信
* @param msg
*/
@RabbitListener(queues={"fanout_sms_queue"})
public void receive_sms(String msg){
System.out.println("短信消费者获取生产者消息msg:" + msg);
}
}
10、消息确认机制
问题产生背景: 生产者发送消息出去之后,不知道到底有没有发送到RabbitMQ服务器, 默认是不知道的。而且有的时候我们在发送消息之后,
后面的逻辑出问题了,我们不想要发送之前的消息了,需要撤回该怎么做。
解决方案: 1.AMQP 事务机制
2.Confirm 模式
事务模式:
txSelect 将当前
channel设置为
transaction模式
txCommit 提交当前事务
txRollback 事务回滚
11、RabbitMQ消息重试机制
消费者在消费消息的时候,如果消费者业务逻辑出现程序异常,这时候应该如何处理?
答案:使用消息重试机制。(演示重试机制)
如何合适选择重试机制:
情况1: 消费者获取到消息后,调用第三方接口,但接口暂时无法访问,是否需要重试? (需要重试机制)
情况2: 消费者获取到消息后,抛出数据转换异常,是否需要重试?(不需要重试机制)需要发布进行解决。
如何实现重试机制 总结:
对于情况2,如果消费者代码抛出异常是需要发布新版本才能解决的问题,那么不需要重试,重试也无济于事。应该采用日志记录+定时任务job健康检查+人工进行补偿
重试机制案例:
生产者代码就按照上面的案例就可以了,
消费者:
yml文件
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_yehui
listener:
simple:
retry:
####开启消费者重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 3000
server:
port: 8081
/**
* 监听队列
*/
@Component
public class ReceiveHandler {
/**
* 邮箱
* @param msg
*/
//rabbitmq 默认情况下 如果消费者程序出现异常的情况下,会自动实现补偿机制
//重试机制都是间隔性的
// 补偿(重试机制) 队列服务器 发送补偿请求
// 如果消费端 程序业务逻辑出现异常消息会消费成功吗? 是不能消费者成功的
//@RabbitListener 底层 使用Aop进行拦截,如果程序没有抛出异常,自动提交事务
// 如果Aop使用异常通知拦截 获取异常信息的话,自动实现补偿机制 ,该消息会缓存到rabbitmq服务器端进行存放,一直重试到不抛异常为准。
// 修改重试机制策略 一般默认情况下 间隔5秒重试一次
@RabbitListener(queues={"fanout_eamil_queue"})
public void receive_email(String msg){
System.out.println("出现异常");
int i = 1/0;
System.out.println("邮箱消费者获取生产者消息msg:" + msg);
}
/**
* 短信
* @param msg
*/
@RabbitListener(queues={"fanout_sms_queue"})
public void receive_sms(String msg){
System.out.println("短信消费者获取生产者消息msg:" + msg);
}
}
调用第三方接口重试机制分析图:
重试机制调用第三方接口 @RabbitListener(queues = "fanout_email_queue") public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception { String messageId = message.getMessageProperties().getMessageId(); String msg = new String(message.getBody(), "UTF-8"); System.out.println("邮件消费者获取生产者消息msg:" + msg + ",消息id:" + messageId); // 重试机制都是间隔性 JSONObject jsonObject = JSONObject.parseObject(msg); String email = jsonObject.getString("email"); String emailUrl = "http://127.0.0.1:8083/sendEmail?email=" + email; System.out.println("邮件消费者开始调用第三方邮件服务器,emailUrl:" + emailUrl); JSONObject result = HttpClientUtils.httpGet(emailUrl); // 如果调用第三方邮件接口无法访问,如何实现自动重试. if (result == null) { throw new Exception("调用第三方邮件服务器接口失败!"); } System.out.println("邮件消费者结束调用第三方邮件服务器成功,result:" + result + "程序执行结束"); // 手动ack Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG); // 手动签收 channel.basicAck(deliveryTag, false); } // 默认是自动应答模式 }
12、消费者如果保证消息幂等性,不被重复消费
产生原因:网络延迟传输中,消费出现异常或者是消费延迟消费,会造成MQ进行重试补偿,
在重试过程中,可能会造成重复消费。 消费者如何保证消息幂等性,
不被重复消费 解决办法:
①使用全局MessageID判断消费方使用同一个,解决幂等性。
②或者使用业务逻辑保证唯一(比如订单号码)
基于全局消息id区分消息,解决幂等性(重复消费)
生产者:
@RequestMapping("/send")
public String send(){
String msg = "my_fanout_msg:" + System.currentTimeMillis();
//设置全局ID
Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8").setMessageId(UUID.randomUUID() + "").build();
System.out.println(msg + ":" + msg);
fanoutProducer.send(message);
return null;
}
消费者
/**
* 邮箱 使用全局ID
* @param msg
*/
@RabbitListener(queues={"fanout_eamil_queue"})
public void receive_email(Message message){
System.out.println("出现异常");
String messageId = message.getMessageProperties().getMessageId();
int i = 1/0;
System.out.println("邮箱消费者获取生产者消息msg:" + messageId);
}
yml文件
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_yehui
listener:
simple:
retry:
####开启消费者重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 3000
server:
port: 8081
启动测试,重试的时候没有发生变化
13、SpringBoot整合RabbitMQ签收模式
//邮件队列
@Component
public class FanoutEamilConsumer {
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {
System.out
.println(Thread.currentThread().getName() + ",邮件消费者获取生产者消息msg:" + new String(message.getBody(), "UTF-8")
+ ",messageId:" + message.getMessageProperties().getMessageId());
// 手动ack
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 手动签收
channel.basicAck(deliveryTag, false);
}
}
开启手动应答
pring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_host
listener:
simple:
retry:
####开启消费者异常重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 2000
####开启手动ack
acknowledge-mode: manual