RabbitMQ使用教程(三)如何保证消息99.99%被发送成功?
1. 前情回顾
RabbitMQ使用教程(一)RabbitMQ环境安装配置及Hello World示例
RabbitMQ使用教程(二)RabbitMQ用户管理,角色管理及权限设置
在以上两篇博客发布后不久,有细心的网友就评论,创建的队列和发送的消息,如果在没有启动消费者程序的时候,重启了RabbitMQ服务,队列和消息都丢失了。
这就引出了一个非常重要的问题,也是面试中经常会问的:在使用RabbitMQ时,如何保证消息最大程度的不丢失并且被正确消费?
2. 本篇概要
RabbitMQ针对这个问题,提供了以下几个机制来解决:
- 生产者确认
- 持久化
- 手动Ack
本篇博客我们先讲解下生产者确认机制,剩余的机制后续单独写博客进行讲解。
3. 生产者确认
要想保证消息不丢失,首先我们得保证生产者能成功的将消息发送到RabbitMQ服务器。
但在之前的示例中,当生产者将消息发送出去之后,消息到底有没有正确地到达服务器呢?如果不进行特殊配置,默认情况下发送消息的操作是不会返回任何消息给生产者的,也就是默认情况下生产者是不知道消息有没有正确的到达服务器。
从basicPublish方法的返回类型我们也能知晓:
public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
this.basicPublish(exchange, routingKey, false, props, body);
}
为了更好理解,我们将之前的生产者Producer类中的channel.queueDeclare(QUEUE_NAME, false, false, false, null);
注释:
package com.zwwhnly.springbootaction.rabbitmq.helloworld;
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 Producer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 指定一个队列,不存在的话自动创建
//channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 发送消息
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
// 关闭频道和连接
channel.close();
connection.close();
}
}
此时运行代码,因为队列不存在,消息肯定没地方存储,但是程序却并未出错,也就是消息丢失了但是我们却并不知晓。
RabblitMQ针对这个问题,提供了两种解决方案:
- 通过事务机制实现
- 通过发送方确认(publisher confirm)机制实现
4. 事务机制
RabblitMQ客户端中与事务机制相关的方法有以下3个:
- channel.txSelect:用于将当前的信道设置成事务模式
- channel.txCommit:用于提交事务
- channel.txRollback:用于回滚事务
新建事务生产者类TransactionProducer,代码如下:
package com.zwwhnly.springbootaction.rabbitmq.producerconfirm;
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 TransactionProducer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 指定一个队列,不存在的话自动创建
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.txSelect();
// 发送消息
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
channel.txCommit();
System.out.println(" [x] Sent '" + message + "'");
// 关闭频道和连接
channel.close();
connection.close();
}
}
运行代码,发现队列新增成功,消息发送成功:
稍微修改下代码,看下异常机制的事务回滚:
try {
channel.txSelect();
// 发送消息
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
int result = 1 / 0;
channel.txCommit();
System.out.println(" [x] Sent '" + message + "'");
} catch (IOException e) {
e.printStackTrace();
channel.txRollback();
}
因为int result = 1 / 0;
肯定会触发java.lang.ArithmeticException异常,所以事务会回滚,消息发送失败:
如果要发送多条消息,可以将channel.basicPublish,channel.txCommit等方法放在循环体内,如下所示:
channel.txSelect();
int loopTimes = 10;
for (int i = 0; i < loopTimes; i++) {
try {
// 发送消息
String message = "Hello World!" + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
channel.txCommit();
System.out.println(" [x] Sent '" + message + "'");
} catch (IOException e) {
e.printStackTrace();
channel.txRollback();
}
}
虽然事务能够解决消息发送方和RabbitMQ之间消息确认的问题,只有消息成功被RabbitMQ接收,事务才能提交成功,否则便可在捕获异常之后进行事务回滚。但是使用事务机制会“吸干”RabbitMQ的性能,因此建议使用下面讲到的发送方确认机制。
5. 发送方确认机制
发送方确认机制是指生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到RabbitMQ服务器之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。
如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该nack指令。
如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。
事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续发送下一条消息。
相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息。生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认后,生产者应用程序便可以通过回调方法来处理该确认消息。
5.1 普通confirm
新建确认生产类NormalConfirmProducer,代码如下:
package com.zwwhnly.springbootaction.rabbitmq.producerconfirm;
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 NormalConfirmProducer {
private final static String EXCHANGE_NAME = "normal-confirm-exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 创建一个Exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
try {
channel.confirmSelect();
// 发送消息
String message = "normal confirm test";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
if (channel.waitForConfirms()) {
System.out.println("send message success");
} else {
System.out.println("send message failed");
// do something else...
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭频道和连接
channel.close();
connection.close();
}
}
channel.confirmSelect();将信道设置成confirm模式。
channel.waitForConfirms();等待发送消息的确认消息,如果发送成功,则返回ture,如果发送失败,则返回false。
如果要发送多条消息,可以将channel.basicPublish,channel.waitForConfirms等方法放在循环体内,如下所示:
channel.confirmSelect();
int loopTimes = 10;
for (int i = 0; i < loopTimes; i++) {
try {
// 发送消息
String message = "normal confirm test" + i;
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
if (channel.waitForConfirms()) {
System.out.println("send message success");
} else {
System.out.println("send message failed");
// do something else...
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
运行结果:
send message success
send message success
send message success
send message success
send message success
send message success
send message success
send message success
send message success
send message success
如果不开启信道的confirm模式,调用channel.waitForConfirms()会报错:
注意事项:
1)事务机制和publisher confirm机制是互斥的,不能共存。
如果企图将已开启事务模式的信道再设置为publisher confirm模式,RabbitMQ会报错:
channel.txSelect();
channel.confirmSelect();
如果企图将已开启publisher confirm模式的信道再设置为事务模式,RabbitMQ也会报错:
channel.confirmSelect();
channel.txSelect();
2)事务机制和publisher confirm机制确保的是消息能够正确地发送至RabbitMQ,这里的“发送至RabbitMQ”的含义是指消息被正确地发往至RabbitMQ的交换器,如果此交换器没有匹配的队列,那么消息也会丢失。所以在使用这两种机制的时候要确保所涉及的交换器能够有匹配的队列。
比如上面的NormalConfirmProducer类发送的消息,发送到了交换器normal-confirm-exchange,但是该交换器并没有绑定任何队列,从业务角度来讲,消息仍然是丢失了。
普通confirm模式是每发送一条消息后就调用channel.waitForConfirms()方法,之后等待服务端的确认,这实际上是一种串行同步等待的方式。因此相比于事务机制,性能提升的并不多。
5.2 批量confirm
批量confirm模式是每发送一批消息后,调用channel.waitForConfirms()方法,等待服务器的确认返回,因此相比于5.1中的普通confirm模式,性能更好。
但是不好的地方在于,如果出现返回Basic.Nack或者超时情况,生产者客户端需要将这一批次的消息全部重发,这样会带来明显的重复消息数量,如果消息经常丢失,批量confirm模式的性能应该是不升反降的。
代码示例:
package com.zwwhnly.springbootaction.rabbitmq.producerconfirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;
public class BatchConfirmProducer {
private final static String EXCHANGE_NAME = "batch-confirm-exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 创建一个Exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
int batchCount = 100;
int msgCount = 0;
BlockingQueue blockingQueue = new ArrayBlockingQueue(100);
try {
channel.confirmSelect();
while (msgCount <= batchCount) {
String message = "batch confirm test";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
// 将发送出去的消息存入缓存中,缓存可以是一个ArrayList或者BlockingQueue之类的
blockingQueue.add(message);
if (++msgCount >= batchCount) {
try {
if (channel.waitForConfirms()) {
// 将缓存中的消息清空
blockingQueue.clear();
} else {
// 将缓存中的消息重新发送
}
} catch (InterruptedException e) {
e.printStackTrace();
// 将缓存中的消息重新发送
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
// 关闭频道和连接
channel.close();
connection.close();
}
}
5.3 异步confirm
异步confirm模式是在生产者客户端添加ConfirmListener回调接口,重写接口的handAck()和handNack()方法,分别用来处理RabblitMQ回传的Basic.Ack和Basic.Nack。
这两个方法都有两个参数,第1个参数deliveryTag用来标记消息的唯一序列号,第2个参数multiple表示的是是否为多条确认,值为true代表是多个确认,值为false代表是单个确认。
示例代码:
package com.zwwhnly.springbootaction.rabbitmq.producerconfirm;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.TimeoutException;
public class AsyncConfirmProducer {
private final static String EXCHANGE_NAME = "async-confirm-exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 创建一个Exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
int batchCount = 100;
long msgCount = 1;
SortedSet<Long> confirmSet = new TreeSet<Long>();
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Ack,SeqNo:" + deliveryTag + ",multiple:" + multiple);
if (multiple) {
confirmSet.headSet(deliveryTag - 1).clear();
} else {
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);
}
// 注意这里需要添加处理消息重发的场景
}
});
// 演示发送100个消息
while (msgCount <= batchCount) {
long nextSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish(EXCHANGE_NAME, "", null, "async confirm test".getBytes());
confirmSet.add(nextSeqNo);
msgCount = nextSeqNo;
}
// 关闭频道和连接
channel.close();
connection.close();
}
}
运行结果:
Ack,SeqNo:1,multiple:false
Ack,SeqNo:2,multiple:false
Ack,SeqNo:3,multiple:false
Ack,SeqNo:4,multiple:false
Ack,SeqNo:5,multiple:false
Ack,SeqNo:6,multiple:false
Ack,SeqNo:7,multiple:false
Ack,SeqNo:8,multiple:false
Ack,SeqNo:9,multiple:false
Ack,SeqNo:10,multiple:false
注意:多次运行,发现每次运行的输出结果是不一样的,说明RabbitMQ端回传给生产者的ack消息并不是以固定的批量大小回传的。
6. 性能比较
到目前为止,我们了解到4种模式(事务机制,普通confirm,批量confirm,异步confirm)可以实现生产者确认,让我们来对比下它们的性能,简单修改下以上示例代码中发送消息的数量,比如10000条,以下为4种模式的耗时:
发送10000条消息,事务机制耗时:2103
发送10000条消息,普通confirm机制耗时:1483
发送10000条消息,批量confirm机制耗时:281
发送10000条消息,异步confirm机制耗时:214
可以看出,事务机制最慢,普通confirm机制虽有提升但是不多,批量confirm和异步confirm性能最好,大家可以根据自己喜好自行选择使用哪种机制,个人建议使用异步confirm机制。
7. 源码及参考
源码地址:https://github.com/zwwhnly/springboot-action.git,欢迎下载。
朱忠华《RabbitMQ实战指南》
RabbitMQ使用教程(三)如何保证消息99.99%被发送成功?的更多相关文章
- RabbitMQ官方教程三 Publish/Subscribe(GOLANG语言实现)
RabbitMQ官方教程三 Publish/Subscribe(GOLANG语言实现) 在上一个教程中,我们创建了一个工作队列. 工作队列背后的假设是,每个任务都恰好交付给一个worker处理. 在这 ...
- RabbitMQ入门教程(三):Hello World
原文:RabbitMQ入门教程(三):Hello World 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog. ...
- RabbitMQ入门教程(十二):消息确认Ack
原文:RabbitMQ入门教程(十二):消息确认Ack 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csd ...
- RabbitMQ-如何保证消息在99.99%的情况下不丢失
1. 简介 MQ虽然帮我们解决了很多问题,但是也带来了很多问题,其中最麻烦的就是,如何保证消息的可靠性传输. 我们在聊如何保证消息的可靠性传输之前,先考虑下哪些情况下会出现消息丢失的情况. 首先,上图 ...
- 《RabbitMQ》如何保证消息的可靠性
一条消费成功被消费经历了生产者->MQ->消费者,因此在这三个步骤中都有可能造成消息丢失. 一 消息生产者没有把消息成功发送到MQ 1.1 事务机制 AMQP协议提供了事务机制,在投递消息 ...
- RabbitMQ保证消息的顺序性
当我们的系统中引入了MQ之后,不得不考虑的一个问题是如何保证消息的顺序性,这是一个至关重要的事情,如果顺序错乱了,就会导致数据的不一致. 比如:业务场景是这样的:我们需要根据mysql的b ...
- RabbitMQ+PHP教程
RabbitMQ+PHP 教程一(Hello World) RabbitMQ+PHP 教程二(Work Queues) RabbitMQ+PHP 教程三(Publish/Subscribe) Rabb ...
- Pulsar の 保证消息的顺序性、幂等性和可靠性
原文链接:Pulsar の 保证消息的顺序性.幂等性和可靠性 一.背景 前面两篇文章,已经介绍了关于Pulsar消费者的详细使用和自研的Pulsar组件. 接下来,将简单分析如何保证消息的顺序性.幂等 ...
- RabbitMQ使用教程(四)如何通过持久化保证消息99.99%不丢失?
1. 前情回顾 RabbitMQ使用教程(一)RabbitMQ环境安装配置及Hello World示例 RabbitMQ使用教程(二)RabbitMQ用户管理,角色管理及权限设置 RabbitMQ使用 ...
随机推荐
- 人物-IT-任正非:任正非
ylbtech-人物-IT-任正非:任正非 任正非,祖籍浙江省浦江县,1944年10月25日出生于贵州省安顺市镇宁县.华为技术有限公司主要创始人兼总裁. 1963年就读于重庆建筑工程学院(现已并入重庆 ...
- docker Get started part 4: Accessing your cluster cannot curl
1. 问题描述 docker Get started part 4 can't visit myvm1 or myvm2. curl: (7) Failed to connect to 192.168 ...
- Javascript作用域和变量提升
下面的程序是什么结果? var foo = 1; function bar() { if (!foo) { var foo = 10; } alert(foo); } bar(); 结果是10: 那么 ...
- 关于UI性能优化
1.使用已经有的VIEW,而不是每次都去新生成一个 2.创建自定义类来进行组件和数据的缓存,在下一次调用的时候直接从FLAG中取出 3.分页,预加载 使用VIEWSTUB进行调用时加载 VIEWSTU ...
- CSS学习系列4 -- 再说CSS中的浮动运用及clear:left/right实际用法
在 CSS学习系列2 -- CSS中的清除浮动 中,我们详细说了CSS中清除浮动的方法及使用 后来我自己在项目开发一个需要使用浮动的网页时,进行了实际运用,加上后来看到一篇好文章.所以就在这里再次写篇 ...
- hdu1068
#include<stdio.h>#include<string.h>const int MAXN=1000;int map[MAXN][MAXN];int n;int lin ...
- SuperSocket使用 IRequestInfo 和 IReceiveFilter 等对象实现自定义协议
为什么你要使用自定义协议? 通信协议用于将接收到的二进制数据转化成您的应用程序可以理解的请求. SuperSocket提供了一个内置的通信协议“命令行协议”定义每个请求都必须以回车换行"\r ...
- 利用superlance监控supervisor运行状态
此文已由作者张家裕授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 最近开发问到supervisor管理下的进程重启了,有无办法做到主动通知,楼主最先想到的是superviso ...
- 在xcode中设置include和lib路径
最近刚刚开始玩xcode,对着教程学编程时很少要动到项目设置,但昨天晚上想使用freetype验证上篇博文的问题,就需要设置include和lib路径了. 首先我下了freetype的源码,并在本地编 ...
- 一个MySQL中两表联合update的例子(并带有group by分组)
内容简介 本文主要展示了在MySQL中,使用两表联合的方式来更新其中一个表字段值的SQL语句. 也就是update table1 join table2 on table1.col_name1=tab ...