AMQP和JMS一样,也是一个消息规范。你可能会想,已经有了JMS(Java Message Service) 。为什么还需要一个AMQP。当然是因为AMQP具备了更多优势了。
在JMS中,通道有助于解耦消息的生产者和消费者,但是这两者依然会和通道相耦合。生产者将消息发布到一个特定的队列或者主题中,消费者从特定的队列或主题中接收消息。通道具有了双重责任。而与之不同的是AMQP的生产者并不会直接将消息发布到队列中,AMQP在消息的生产者以及传递信息的队列之间引入了一种间接的机制:Exchange。消息的生产者将信息发布到一个Exchange上。Exchange会绑定到一个或多个队列上,他负责将信息路由到队列上。信息的消费者会从队列中提取数据并进行处理。
AMQP四种不同的Exchange
标准
概念
Direct
如果消息的 routing key 与 binding 的routeing key直接匹配的话,消息将会路由到该队列上
Topic
如果消息的 routing key 与 binding 的routeing key符合通配符匹配的话,消息将会路由到该队列上
Headers
如果消息参数表中的头信息和值都与binding参数表中匹配的话,消息将会路由到该队列上
Fanout
不管消息的routing key和参数表的头信息/值是什么,消息将会路由到所有队列上
借助上面四种类型的Exchange,可以定义出不再仅限于点对点和发布-订阅的方式。
AMQP 和 JMS 的区别:
RabbitMQ 是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了一个经纪人(Broker)构架,这意味着消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)或者数据持久化都有很好的支持。
RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。 在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。
RabbitMQ与邮局的主要区别是它不处理纸张,而是接受,存储和转发数据消息的二进制数据块。
生产者:
(1) 生产者连接到RabbitMQ Broker,建立一个连接( Connection)开启一个信道(Channel)
(2) 生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等
(3) 生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除等
(4) 生产者通过路由键将交换器和队列绑定起来
(5) 生产者发送消息至RabbitMQ Broker,其中包含路由键、交换器等信息。
(6) 相应的交换器根据接收到的路由键查找相匹配的队列。
(7) 如果找到,则将从生产者发送过来的消息存入相应的队列中。
(8) 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
(9) 关闭信道。
(10) 关闭连接。
消费者:
(1) 消费者连接到RabbitMQ Broker ,建立一个连接(Connection),开启一个信道(Channel) 。
(2) 消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数,
(3) 等待RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息。
(4) 消费者确认(ack) 接收到的消息。
(5) RabbitMQ 从队列中删除相应己经被确认的消息。
(6) 关闭信道。
(7)关闭连接。
RabbitMQ 提供了5种消息模式(其实是6种,只不过第六种属于RPC,所以不在此讨论)。五种消费模型分别是 基本消息模型、工作消费模型、Fanout订阅模型、Direct订阅模型、Topic订阅模型。。其中后三种都属于订阅模型。
先做些准备工作:
1 创建虚拟节点
RabbitMQ 管理平台默认地址: http://localhost:15672
默认用户名密码都是guest。
其实这一步创不创建无所谓
2 创建RabbitMQ连接工具类
这个工具类就是和RabbitMQ建立了连接,否则后面要写很多重复代码。
/**
* @Data: 2019/12/18
* @Des:
*/
public class RabbitMQUtils {
public static Connection getConection() throws IOException, TimeoutException {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/hello");
factory.setUsername("kingfish");
factory.setPassword("kingfish");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
下面开始介绍五种消息模型:
生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。
需要注意的是: 虽然架构图是这样画,但是本质上的信息还是通过交换机(Exchange)。在不声明交换机的情况下,使用的是RabbitMQ默认的交换机。
1、生产者代码如下:
package com.kingfish.test.rabbitmq.basic;
import com.kingfish.test.rabbitmq.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Data: 2019/12/18
* @Des: 基本消息模型 生产者
*/
public class BasicProducer {
private static final String QUEUE_NAME = "BaseQueueName";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 在通道中创建一个队列
// 【参数说明:参数一:队列名称,参数二:是否持久化;参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数】
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 消息内容
String message = "info : 地瓜地瓜";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("### 消息已发送 : " + message);
//关闭通道和连接
channel.close();
connection.close();
}
}
2 . 执行生产者后,我们可以看到有一条消息已经发送到了队列中
3. 查看队列中的信息,可以看到,这个消息被发送到了默认的交换机(Exchange) 上,且RoutingKey(默认) 即队列名。
4. 消费者 代码如下
package com.kingfish.test.rabbitmq.basic;
import com.kingfish.test.rabbitmq.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Data: 2019/12/18
* @Des: 基本消息模型,消费者
*/
public class BasicConsumer {
private static final String QUEUE_NAME = "BaseQueueName";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 在通道中创建一个队列。如果通道中已经存在该队列,则不会重新创建。
// 如果队列中没有该队列直接绑定会报错,所以生产者消费者中都需要声明队列
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 {
// body 即消息体
String msg = new String(body);
System.out.println("地瓜消费者001 收到消息 : " + msg);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
5. 可以看到队列中消息确实被消费了。并且消费者确实接受到了消息。
基本消费模型架构图:
在实际应用中,我们的消费者可能是集群消费。所以形成了工作消费模型。(实际上我们的生产者可能也是个集群)。
1、生产者代码如下:
这里的代码和上面的唯一的区别就是加了一个循环,发送了50次消息
public class WorkProducer {
private static final String QUEUE_NAME = "WorkQueueName";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 在通道中创建一个队列
// 【参数说明:参数一:队列名称,参数二:是否持久化;参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数】
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 50; i++) {
// 消息内容
String message = "info : 地瓜地瓜 " + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("### 消息已发送 : " + message);
}
//关闭通道和连接
channel.close();
connection.close();
}
}
2. 消费者1代码如下:
(消费者2 的代码和消费者1完全相同,这里就不在给出
public class WorkConsumer1 {
private static final String QUEUE_NAME = "WorkQueueName";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 在通道中创建一个队列。如果通道中已经存在该队列,则不会重新创建。
// 如果队列中没有该队列直接绑定会报错,所以生产者消费者中都需要声明队列
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 {
// body 即消息体
String msg = new String(body);
System.out.println("地瓜消费者001 收到消息 : " + msg);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
注:
由于RabbitMQ的机制,会先均分给两个消费者消息。这时候如果两个消费者由于性能或其他问题处理速度不同,就会造成一个消费者早已经处理结束分配的消息,另一个消费者还在处理。这显然是不合理的。所以通过channel.basicQos(1);
设置每次值拉取一个消息。即每次只吃一个,吃完再拿,避免了上述情况。
后面的三种都属于订阅模型,只不过规则不同而已。
在之前的模式中,我们创建了一个工作队列。 工作队列背后的假设是:每个任务只被传递给一个工作人员。 在这一部分,我们将做一些完全不同的事情 - 我们将会传递一个信息给多个消费者。 这种模式被称为“发布/订阅”。
1、1个生产者,多个消费者
2、每一个消费者都有自己的一个队列
3、生产者没有将消息直接发送到队列,而是发送到了交换机
4、每个队列都要绑定到交换机
5、生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的
X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
Exchange类型有以下几种:
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
需要注意:Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
在广播模式下,消息发送流程是这样的:
1. 生产者代码
Fanout 模式下 : 生产者的代码中不在声明队列,直接把消息发送给交换机,交换机发送给所有绑定的队列
public class FanoutProducer {
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 声明exchange,指定类型为fanout
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息内容
String message = "info : 地瓜地瓜 ";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("### 消息已发送 : " + message);
//关闭通道和连接
channel.close();
connection.close();
}
}
2. 消费者代码
两个消费者代码相同,这里只贴出一个)
public class FanoutConsumer1 {
private static final String EXCHANGE_NAME = "fanout_exchange";
private static final String QUEUE_NAME = "fanout_queue_name1";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 在通道中创建一个队列。如果通道中已经存在该队列,则不会重新创建。
// 如果队列中没有该队列直接绑定会报错,所以生产者消费者中都需要声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 声明交换机,若存在则不重新声明
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 设置消费者一次只能拉取一个消息
channel.basicQos(1);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println("地瓜消费者001 收到消息 : " + msg);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
有选择性的接收消息
在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
在路由模式中,我们将添加一个功能 - 我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。
1. 生产者代码
public class DirectProducer {
private static final String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 声明exchange,指定类型为direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 消息内容
String message = "info : 地瓜地瓜 ";
channel.basicPublish(EXCHANGE_NAME, "info", null, message.getBytes());
System.out.println("### 消息已发送 : " + message);
//关闭通道和连接
channel.close();
connection.close();
}
}
2. 消费者代码
public class DirectConsumer1 {
private static final String EXCHANGE_NAME = "direct_exchange";
private static final String QUEUE_NAME = "direct_queue_name1";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 在通道中创建一个队列。如果通道中已经存在该队列,则不会重新创建。
// 如果队列中没有该队列直接绑定会报错,所以生产者消费者中都需要声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 声明交换机,若存在则不重新声明
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 绑定队列到交换机。绑定的routingKey为 info 和error。即
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
// 设置消费者一次只能拉取一个消息
channel.basicQos(1);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println("地瓜消费者001 收到消息 : " + msg);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消费者2 与消费者1的不同之处 : 消费者2绑定了info和error的routingKey。
上述代码执行后,消费者2可以接收到生产者的消息。而消费者1不可以。因为消费者1绑定的 routingKey 是error。而生产者发送的routeringKey 是info。
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
如:
audit.#
:能够匹配audit.irs.corporate
或者 audit.irs
audit.*
:只能匹配audit.irs
1. 生产者代码:
public class TopicProducer {
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 声明exchange,指定类型为fanout
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容
String message = "info : 地瓜地瓜 ";
channel.basicPublish(EXCHANGE_NAME, "www.baidu.com", null, message.getBytes());
System.out.println("### 消息已发送 : " + message);
//关闭通道和连接
channel.close();
connection.close();
}
}
2. 消费者1 代码
package com.kingfish.test.rabbitmq.topic;
import com.kingfish.test.rabbitmq.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Data: 2019/12/18
* @Des:
*/
public class TopicConsumer1 {
private static final String EXCHANGE_NAME = "topic_exchange";
private static final String QUEUE_NAME = "topic_queue_name1";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = RabbitMQUtils.getConection();
// 从连接中创建通道。后面大部分的操作都是通过通道完成
Channel channel = connection.createChannel();
// 在通道中创建一个队列。如果通道中已经存在该队列,则不会重新创建。
// 如果队列中没有该队列直接绑定会报错,所以生产者消费者中都需要声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 声明交换机,若存在则不重新声明
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "www.#");
// 设置消费者一次只能拉取一个消息
channel.basicQos(1);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println("地瓜消费者001 收到消息 : " + msg);
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
上面的例子中,生产者生产消息后,消费者1和2都会收到消息。因为消费者2 routingKey 是 www.#
消费者2 routingKey 是 ww.baidu.*。结合上面说的 #
匹配若干,*
匹配唯一不难推论出来。
更为详细的可靠性文章请移步(本章节部分参考该文): https://mp.weixin.qq.com/s/dYH0wAYYiXuQwiBqc7wMjg
RabbitMQ 整个工作流程可以分为以下三步:
第一步:生产端到RabbitMQ
这一步可以通过事务机制或者Comfirm机制来保证。需要注意的是,事务机制虽然保证了消息投递端的可靠性,但因为每次投递都开启了事务,所以性能较低,一般不推荐使用,一般使用 Confirm 机制。
第二步:RabbitMQ
这一步是保证 RabbitMQ 由于某些故障消息如何保证不丢失。
普通模式 : 普通模式下,集群中的 RabbitMQ 会同步 Vhost、Exchange、Binding、Queue 的元数据(即其本身的数据,例如名称、属性等)以及 Message 结构,而不会同步 Message 数据,也就是说,如果集群中某台机器 RabbitMQ 宕掉了,则该节点上的 Message 不可用,直至该节点恢复。
镜像模式
镜像队列 :相当于配置了副本,绝大多数分布式的东西都有多副本的概念来确保 HA(High Availability)。在镜像队列中,如果主节点(master)在此特殊时间内挂掉,可以自动切换到从节点(slave),这样有效的保证了高可用性,除非整个集群都挂掉。
第三步:RabbitMQ到消费者
这一步是为了保证消息被正确的消费,因此我们可以使用手动签收的机制
为了保证消息从队列可靠地达到消费者,RabbitMQ 提供了消息确认机制(message acknowledgement)。消费者在订阅队列时,可以指定 autoAck 参数,当 autoAck 等于 fals e时,RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。当 autoAck 等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正的消费到了这些消息。
对于 RabbitMQ 而言,队列中的消息分成了两个部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果 RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则 RabbitMQ 会安排该消息重新进入队列,等待投递给下一个消费者。RabbitMQ 判断此消息是否需要重新投递的唯一依据是消费该消息的消费者连接是否已经断开,这种设计允许消费者消费一条消息很久很久。
如果消息消费失败,也可以调用 Basic.Reject 或者 Basic.Nack 来拒绝当前消息,但需要注意的是,如果只是简单的拒绝那么消息将会丢失,需要将相应的 requeue 参数设置为 true,RabbitMQ 才会将这条消息重新存入队列。而如果 requeue 参数设置为 false 的话,RabbitMQ 立即会把消息从队列中移除,而不会把它发送给新的消费者。
basicNack 和 basicReject 作用基本相同,主要差别在于前者可以拒绝多条,后者只能拒绝单条,另外basicNack 不是 AMQP 0-9-1 标准。
// 确认消息
channel.basicAck(deliveryTag, multiple);
// 拒绝消息
channel.basicNack(deliveryTag, multiple, requeue);
// 拒绝消息
channel.basicReject(deliveryTag, requeue)
上面介绍的五种消息模型,本质上都是通过交换机绑定队列进行交互。所以当RabbitMQ宕机后消息就会丢失。所以我们需要持久化保存消息。
想要持久化消息就要先持久化队列,想要持久化队列就要先持久化交换机。
交换机的持久化 :
RabbitMQ中对生产者提供了两种签收机制 : 事务机制和Confirm机制
通过 AMQP 事务机制实现,这也是 AMQP 协议层面提供的解决方案
RabbitMQ 中与事务机制有关的方法有三个:txSelect(), txCommit()以及 txRollback(),
txSelect 用于将当前 channel 设置成 transaction 模式 txCommit 用于提交事务,
txRollback 用于回滚事务,在通过 txSelect 开启事务之后,我们便可以发布消息给 broker 代理服务器了,如果
txCommit 提交成功了,则消息一定到达了 broker 了 如果在 txCommit执行之前 broker
异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过 txRollback 回滚事务了。
关键代码:
channel.txSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
channel.txCommit();
生产者: 主要部分
try {
// 启用事务模式
channel.txSelect();
// 发送内容【参数说明:参数一:交换机名称;参数二:队列名称,参数三:消息的其他属性-routing headers,此属性为MessageProperties.PERSISTENT_TEXT_PLAIN用于设置纯文本消息存储到硬盘;参数四:消息主体】
channel.basicPublish("exchangeKey_direct", "routerKey1", null, content.getBytes("UTF-8"));
System.out.println("已发送消息:" + content);
int i = 1/ 0;
// 事务提交
channel.txCommit();
} catch (Exception e) {
// 事务回滚
channel.txRollback();
e.printStackTrace();
System.out.println("发送出错,消息回滚。" + content);
}
此种模式很耗时且采用这种方式 降低了 Rabbitmq 的消息吞吐量
*通过将 channel 设置成 confirm 模式来实现。需要注意,事务方式和Confirm两种模式不能共存
生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理;
confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息;
在编程中我们可以选择下面的几种编程方式
1、普通 confirm 模式
// 生产者通过调用confirmSelect 方法将 channel 设置为 confirm 模式
channel.confirmSelect();
// 发送内容【参数说明:参数一:交换机名称;参数二:队列名称,参数三:消息的其他属性-routing headers,此属性为MessageProperties.PERSISTENT_TEXT_PLAIN用于设置纯文本消息存储到硬盘;参数四:消息主体】
channel.basicPublish("exchangeKey_direct", "routerKey1", null, content.getBytes("UTF-8"));
// 判断消息是否发送成功
if (channel.waitForConfirms()) {
System.out.println("已发送消息:" + content);
} else {
System.out.println("消息发送失败,消息内容 :" + content);
}
2、批量 confirm 模式
批量 confirm 模式稍微复杂一点,客户端程序需要定期(每隔多少秒)或者定量(达到多少条)或者两则结合起来publish 消息,然后等待服务器端 confirm, 相比普通 confirm 模式,批量极大提升 confirm 效率,但是问题在于一旦出现 confirm 返回 false 或者超时的情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且,当消息经常丢失时,批量 confirm 性能应该是不升反降的。
// 生产者通过调用confirmSelect 方法将 channel 设置为 confirm 模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
// 发送内容【参数说明:参数一:交换机名称;参数二:队列名称,参数三:消息的其他属性-routing headers,此属性为MessageProperties.PERSISTENT_TEXT_PLAIN用于设置纯文本消息存储到硬盘;参数四:消息主体】
channel.basicPublish("exchangeKey_direct", "routerKey1", null, content.getBytes("UTF-8"));
}
// 判断消息是否发送成功
if (channel.waitForConfirms()) {
System.out.println("已发送消息:" + content);
} else {
System.out.println("消息发送失败,消息内容 :" + content);
}
3、异步 confirm 模式
Channel 对象提供的 ConfirmListener()回调方法只包含 deliveryTag(当前 Chanel 发出的消息序号),我们需要自己为每一个 Channel 维护一个 unconfirm 的消息序号集合,每 publish 一条数据,集合中元素加 一,每回调一次 handleAck方法,unconfirm 集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。从程序运行效率上看,这个unconfirm 集合最好采用有序集合 SortedSet 存储结构。
实际上,SDK 中的 waitForConfirms()方法也是通过 SortedSet维护消息序号的。
// 生产者通过调用confirmSelect 方法将 channel 设置为 confirm 模式
channel.confirmSelect();
// 创建一个列表,用于维护消息发送的情况
final SortedSet confirmSet = Collections.synchronizedSortedSet(new TreeSet());
// 添加Confirm监听器,监听消息发送的状态。
channel.addConfirmListener(new ConfirmListener() {
//每回调一次handleAck方法,confirmSet 删掉相应的一条(multiple=false)或多条(multiple=true)记录。
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
System.out.println("移除集合中的多条记录--");
confirmSet.headSet(deliveryTag + 1).clear(); //用一个SortedSet, 返回此有序集合中小于end的所有元素。
} else {
System.out.println("--multiple false--");
confirmSet.remove(deliveryTag);
}
}
// 消息签收失败时调用
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
// System.out.println("Nack, SeqNo: " + deliveryTag + ", multiple: " + multiple);
// if (multiple) {
// confirmSet.headSet(deliveryTag + 1).clear();
// } else {
// confirmSet.remove(deliveryTag);
// }
}
});
while (true) {
// Confirm模式下,返回下一条要发布的消息的序列号。
long nextSeqNo = channel.getNextPublishSeqNo();
Thread.sleep(2000);
channel.basicPublish("exchangeKey_direct", "routerKey1", null, content.getBytes("UTF-8"));
confirmSet.add(nextSeqNo);
}
对于消费者的消息签收有有自动和手动两种。
自动签收 : 并不会管消息的处理即其它问题。类似于签收快递,自动签收则是将快递放入快递柜,至于后续,不管你是否接收,以及快递是否有问题(程序执行不通过),都不会有所反应。
手动签收 : 是由程序确认是否签收。即自己本人签收快递,即细致检查(程序处理)后,如果没有问题,则会确定签收,若有问题,可以拒签。
通过 String basicConsume(String queue, boolean autoAck, String consumerTag, Consumer callback) throws IOException;
方法或其重载方法中的 autoAck
参数设置是否自动签收。为true则自动签收
// 创建通道
Channel channel = conn.createChannel();
// 创建订阅器,并接受消息。 第一个参数 : 队列名, 第二个参数签收方式 : false,设置成手动签收模式;true,则自动签收模式
channel.basicConsume("queueName1", true, "", new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String routingKey = envelope.getRoutingKey(); // 队列名称
String contentType = properties.getContentType(); // 内容类型
String content = new String(body, "utf-8"); // 消息正文
System.out.println("routingKey :" + routingKey + " contentType : " + contentType + " 消息正文:" + content);
// channel.basicAck(envelope.getDeliveryTag(), false); // 手动确认消息【参数说明:参数一:该消息的index;参数二:是否批量应答,true批量确认小于index的消息】
}
});
通过 String basicConsume(String queue, boolean autoAck, String consumerTag, Consumer callback) throws IOException;
方法或其重载方法中的 autoAck
参数设置为false,通过 void basicAck(long deliveryTag, boolean multiple)
方法来进行手动签收。
// 创建通道
Channel channel = conn.createChannel();
// 创建订阅器,并接受消息。 第一个参数 : 队列名, 第二个参数签收方式 : false,设置成手动签收模式。true,则自动签收模式
channel.basicConsume("queueName1", false, "", new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String routingKey = envelope.getRoutingKey(); // 队列名称
String contentType = properties.getContentType(); // 内容类型
String content = new String(body, "utf-8"); // 消息正文
System.out.println("routingKey :" + routingKey + " contentType : " + contentType + " 消息正文:" + content);
// channel.basicAck(envelope.getDeliveryTag(), false); // 手动确认消息【参数说明:参数一:该消息的index;参数二:是否批量应答,true批量确认小于index的消息】
}
});
额外还有一些方法:
//扔掉消息
channel.BasicReject(result.DeliveryTag, false);
//退回消息
channel.BasicReject(result.DeliveryTag, true);
//批量退回或删除,中间的参数 是否批量 true是/false否 (也就是只一条)
channel.BasicNack(result.DeliveryTag, true, true);
//补发消息 true退回到queue中/false只补发给当前的consumer;BasicRecover方法则是进行补发操作,其中的参数如果为true是把消息退回到queue但是有可能被其它的consumer接收到,设置为false是只补发给当前的consumer
channel.BasicRecover(true);
注: 生产者的签收成功指的是消息被RabbitMQ签收,并不是指被消费。
消费者的签收成功指的是RabbitMQ中的消息被消费。
#访问RabbitMQ服务器的账户,默认是guest
rabbitmq.username=guest
#访问RabbitMQ服务器的密码,默认是guest
rabbitmq.password=guest
#RabbitMQ服务器地址,默认值"localhost
rabbitmq.host=localhost
#RabbitMQ服务端口,默认值为5672
rabbitmq.port=5672
#hannel的缓存数量,默认值为25
rabbitmq.channelCacheSize=50
#缓存连接模式,默认值为CHANNEL(单个connection连接,连接之后关闭,自动销毁)
rabbitmq.cacheMode=CHANNEL
创建RabbitMQConfig 配置类:
package com.kingfish.common.config.mq;
import com.kingfish.pojo.handler.RabbitMQHandler;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.UnsupportedEncodingException;
/**
* @Data: 2019/10/9
* @Des:
*/
@Configuration
@PropertySource(value = {"classpath:rabbitmq.properties"})
@EnableRabbit
public class RabbitMQConfig {
@Value("${rabbitmq.username}")
private String username;
@Value("${rabbitmq.password}")
private String password;
@Value("${rabbitmq.host}")
private String host;
@Value("${rabbitmq.port}")
private int port;
@Value("${rabbitmq.channelCacheSize}")
private int channelCacheSize;
@Value("${rabbitmq.cacheMode}")
private String cacheMode;
@Bean
public ConnectionFactory cachingConnectionFactory() {
CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory();
cachingConnectionFactory.setHost(host);
cachingConnectionFactory.setChannelCacheSize(channelCacheSize);
cachingConnectionFactory.setCacheMode(CachingConnectionFactory.CacheMode.CHANNEL);
return cachingConnectionFactory;
}
/**
* 该类封装了对 RabbitMQ 的管理操作
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
// 声明队列
Queue queueName1 = new Queue("queueName1");
rabbitAdmin.declareQueue(queueName1);
// 声明交换机
rabbitAdmin.declareExchange(new DirectExchange("exchangeKey_direct", true, false));
// 使用BindingBuilder进行绑定
rabbitAdmin.declareBinding(BindingBuilder.bind(queueName1).to(new DirectExchange("exchangeKey_direct")).with("routerKey1"));
return rabbitAdmin;
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 设置消息回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("rabbit return success" + message.toString() + "===" + replyText + "===" + exchange + "===" + routingKey);
}
});
// 消息回调结果
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
System.out.println("rabbit 消息发送失败" + cause + correlationData.toString());
} else {
System.out.println("rabbit 消息发送成功 ");
}
});
return rabbitTemplate;
}
@Bean
public RabbitListenerContainerFactory> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
// 设置QOS,保证同一时间一个消费者只能消费一条消息
factory.setPrefetchCount(1);
//初始化消费者数量
factory.setConcurrentConsumers(1);
//最大消费者数量
factory.setMaxConcurrentConsumers(1);
// 设置应答模式
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
/**
* 可以在这个bean中设置接收消息方式
*/
@Bean
public RabbitListenerConfigurer rabbitListenerConfigurer() {
return new RabbitListenerConfigurer() {
@Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
// 方式1 : 直接处理
SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint();
endpoint.setId("0");
endpoint.setQueueNames("queueName1");
endpoint.setMessageListener(message -> {
// 直接在这里处理消息信息
try {
System.out.println("endpoint1处理消息的逻辑 : " + new String(message.getBody(), "utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
//方式2 使用适配器来处理消息 RabbitMQHandler 来处理信息
SimpleRabbitListenerEndpoint endpoint2 = new SimpleRabbitListenerEndpoint();
endpoint2.setId("1");
endpoint2.setQueueNames("queueName1");
System.out.println("endpoint2处理消息的逻辑");
// 绑定pojo,并指定默认处理方法为onMessage
endpoint2.setMessageListener(new MessageListenerAdapter(new RabbitMQHandler(), "onMessage"));
//注册endpoint
registrar.registerEndpoint(endpoint);
registrar.registerEndpoint(endpoint2);
}
};
}
}
public class RabbitMQHandler {
public void onMessage(Object object) {
System.out.println("RabbitMQHandler : " + object.toString());
}
}
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @Data: 2019/10/10
* @Des:
*/
@RestController
@RequestMapping("mq")
public class MQController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("send")
public String sendMessage() {
rabbitTemplate.convertAndSend("exchangeKey_direct", "routerKey1",
"发送的信息" + new SimpleDateFormat("yyyy-MM-dd hh:mm;ss").format(new Date()));
return "ok";
}
@RequestMapping("receive")
public String receiveMessage() throws Exception {
// 接收消息
Message receive = rabbitTemplate.receive("queueName1");
System.out.println("消息正文:" + new String(receive.getBody(), "utf-8"));
return new String(receive.getBody(), "utf-8");
}
}
使用上面的方式,很明显没有办法异步监听消息。如果我们不调用receive接口,便无法接收到rabbit中的消息。而使用消息驱动方式则可以解决这个问题。
RabbitCMQConfig配置类
package com.kingfish.common.config.mq;
import com.kingfish.pojo.handler.RabbitMQHandler;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.io.UnsupportedEncodingException;
/**
* @Data: 2019/10/9
* @Des:
*/
@Configuration
@PropertySource(value= {"classpath:rabbitmq.properties"})
@EnableRabbit
public class RabbitMQConfig {
@Value("${rabbitmq.username}")
private String username;
@Value("${rabbitmq.password}")
private String password;
@Value("${rabbitmq.host}")
private String host;
@Value("${rabbitmq.port}")
private int port;
@Value("${rabbitmq.channelCacheSize}")
private int channelCacheSize;
@Value("${rabbitmq.cacheMode}")
private String cacheMode;
@Bean
public ConnectionFactory cachingConnectionFactory(){
CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory();
cachingConnectionFactory.setHost(host);
cachingConnectionFactory.setChannelCacheSize(channelCacheSize);
return cachingConnectionFactory;
}
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory){
return new RabbitAdmin(connectionFactory);
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setExchange("exchangeKey_direct");
return rabbitTemplate;
}
@Bean
public RabbitListenerContainerFactory> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
return factory;
}
@Bean
public RabbitListenerConfigurer rabbitListenerConfigurer(){
return new RabbitListenerConfigurer() {
@Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
// 方式1 : 直接处理
SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint();
endpoint.setId("0");
endpoint.setQueueNames("queueName1");
endpoint.setMessageListener(message -> {
// 直接在这里处理消息信息
try {
System.out.println("endpoint1处理消息的逻辑 : " + new String(message.getBody(), "utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
//方式2 使用适配器来处理消息 RabbitMQHandler 来处理信息
SimpleRabbitListenerEndpoint endpoint2 = new SimpleRabbitListenerEndpoint();
endpoint2.setId("1");
endpoint2.setQueueNames("queueName1");
System.out.println("endpoint2处理消息的逻辑");
// 绑定pojo,并指定默认处理方法为onMessage
endpoint2.setMessageListener(new MessageListenerAdapter(new RabbitMQHandler(),"onMessage"));
//注册endpoint
registrar.registerEndpoint(endpoint);
registrar.registerEndpoint(endpoint2);
}
};
}
}
消息驱动类 RabbitMQHandler
public class RabbitMQHandler {
public void onMessage(Object object) throws Exception {
// 接收到消息
System.out.println("RabbitMQHandler : " + object.toString());
}
}
当有消息发送时,RabbitMQHandler.onMessage方法来接收信息并进行需要的处理。
程序运行如下:
使用注解方式配置也很简单,声明一个如下的类即可
@Component
// 监听注解,也可以直接写在方法上
@RabbitListener(queues = "queueName1")
public class RabbitMQListener {
@RabbitHandler()
public void process(String hello, Channel channel, Message message) throws IOException {
System.out.println("HelloReceiver收到 : " + hello + "收到时间" + new Date());
try {
//告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 后续还会在发
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
System.out.println("receiver success");
} catch (IOException e) {
e.printStackTrace();
//丢弃这条消息
//channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
System.out.println("receiver fail");
}
}
}
写在方法上 :
@Component
public class Listener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "spring.test.queue", durable = "true"),
exchange = @Exchange(
value = "spring.test.exchange",
ignoreDeclarationExceptions = "true", // 忽略交换机声明异常,使用已有交换机
type = ExchangeTypes.TOPIC // Topic 类型
),
key = {"#.#"})) // 通配符
// 接收的消息是什么类型就用什么类型接收
public void listen(@Payload String msg, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws IOException){
System.out.println("接收到消息:" + msg);
// 手动应答确认消费成功
channel.basicAck(deliveryTag,false);
}
}
上述通过配置类方式来配置,下面是xml配置方式下的配置文件
注:
配置类方式使用xml时需要在 AbstractAnnotationConfigDispatcherServletInitializer 实现类上加上注解@ImportResource 引入配置文件。否则可能无法自动注入xml中声明的bean
以上:内容部分参考
https://www.cnblogs.com/cjm123/p/9679171.html
https://www.cnblogs.com/zhanghaoliang/p/7886110.html
https://www.jianshu.com/p/3d43561bb3ee
https://www.cnblogs.com/vipstone/p/9275256.html
https://blog.csdn.net/qq_27384769/article/details/79615015
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正