RabbitMQ如何保证消息99.99%被发送成功?
1. 本篇概要
RabbitMQ针对这个问题,提供了以下几个机制来解决:
- 生产者确认
- 持久化
- 手动Ack
本篇博客我们先讲解下生产者确认机制,剩余的机制后续单独写博客进行讲解。
2. 生产者确认
要想保证消息不丢失,首先我们得保证生产者能成功的将消息发送到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)机制实现
3. 事务机制
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的性能,因此建议使用下面讲到的发送方确认机制。

4. 发送方确认机制
发送方确认机制是指生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到RabbitMQ服务器之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。
如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该nack指令。
如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。
事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续发送下一条消息。
相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息。生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认后,生产者应用程序便可以通过回调方法来处理该确认消息。
4.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()方法,之后等待服务端的确认,这实际上是一种串行同步等待的方式。因此相比于事务机制,性能提升的并不多。
4.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();
}
}
4.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消息并不是以固定的批量大小回传的。
5. 性能比较
到目前为止,我们了解到4种模式(事务机制,普通confirm,批量confirm,异步confirm)可以实现生产者确认,让我们来对比下它们的性能,简单修改下以上示例代码中发送消息的数量,比如10000条,以下为4种模式的耗时:
发送10000条消息,事务机制耗时:2103 发送10000条消息,普通confirm机制耗时:1483 发送10000条消息,批量confirm机制耗时:281 发送10000条消息,异步confirm机制耗时:214
可以看出,事务机制最慢,普通confirm机制虽有提升但是不多,批量confirm和异步confirm性能最好,大家可以根据自己喜好自行选择使用哪种机制,个人建议使用异步confirm机制。
RabbitMQ如何保证消息99.99%被发送成功?的更多相关文章
- RabbitMQ-如何保证消息在99.99%的情况下不丢失
1. 简介 MQ虽然帮我们解决了很多问题,但是也带来了很多问题,其中最麻烦的就是,如何保证消息的可靠性传输. 我们在聊如何保证消息的可靠性传输之前,先考虑下哪些情况下会出现消息丢失的情况. 首先,上图 ...
- RabbitMQ 如何保证消息不丢失?
RabbitMQ一般情况很少丢失,但是不能排除意外,为了保证我们自己系统高可用,我们必须作出更好完善措施,保证系统的稳定性. 下面来介绍下,如何保证消息的绝对不丢失的问题,下面分享的绝对干货,都是在知 ...
- springboot + rabbitmq发送邮件(保证消息100%投递成功并被消费)
前言: RabbitMQ相关知识请参考: https://www.jianshu.com/p/cc3d2017e7b3 Linux安装RabbitMQ请参考: https://www.jianshu. ...
- rabbitmq如何保证消息可靠性不丢失
目录 生产者丢失消息 代码模拟 事务 confirm模式确实 数据退回监听 MQ事务相关软文推荐 MQ丢失信息 消费者丢失信息 之前我们简单介绍了rabbitmq的功能.他的作用就是方便我们的消息解耦 ...
- (转载)springboot + rabbitmq发送邮件(保证消息100%投递成功并被消费)
转载自https://www.jianshu.com/p/dca01aad6bc8 一.先扔一张图 image.png 说明: 本文涵盖了关于RabbitMQ很多方面的知识点, 如: 消息发送确认 ...
- 解决RabbitMQ消息丢失问题和保证消息可靠性(一)
原文链接(作者一个人):https://juejin.im/post/5d468591f265da03b810427e 工作中经常用到消息中间件来解决系统间的解耦问题或者高并发消峰问题,但是消息的可靠 ...
- 《RabbitMQ》如何保证消息的可靠性
一条消费成功被消费经历了生产者->MQ->消费者,因此在这三个步骤中都有可能造成消息丢失. 一 消息生产者没有把消息成功发送到MQ 1.1 事务机制 AMQP协议提供了事务机制,在投递消息 ...
- RabbitMQ保证消息的顺序性
当我们的系统中引入了MQ之后,不得不考虑的一个问题是如何保证消息的顺序性,这是一个至关重要的事情,如果顺序错乱了,就会导致数据的不一致. 比如:业务场景是这样的:我们需要根据mysql的b ...
- Pulsar の 保证消息的顺序性、幂等性和可靠性
原文链接:Pulsar の 保证消息的顺序性.幂等性和可靠性 一.背景 前面两篇文章,已经介绍了关于Pulsar消费者的详细使用和自研的Pulsar组件. 接下来,将简单分析如何保证消息的顺序性.幂等 ...
随机推荐
- SecureCRT连接本地虚拟机Linux系统很慢
SSH配置问题 cd /etc/ssh/ 备份一下配置文件 cp sshd_config sshd_config.2019-07-17.bak 修改配置 vim sshd_config 重启sshd服 ...
- 使用zabbix监控linux的io
zabbix自带的监控linux的模板中并没有监控io这项,而实际生产中又需要监控io,如何监控呢. 错误的示例 这里我特意贴出错误的示例出来,是因为我在网上搜如何使用zabbix监控io的文章时,好 ...
- Spring Cloud Gateway入坑记
Spring Cloud Gateway入坑记 前提 最近在做老系统的重构,重构完成后新系统中需要引入一个网关服务,作为新系统和老系统接口的适配和代理.之前,很多网关应用使用的是Spring-Clou ...
- Python入门你要懂哪些?
前言 什么是计算机语言 计算机就是一台用来计算的机器,人让计算机干什么计算机就得干什么! 需要通过计算机的语言来控制计算机(也就是编程语言)! 计算机语言其实和人类的语言没有本质的区别,不同点就是交流 ...
- Java生鲜电商平台-B2B生鲜的互联网思维
Java生鲜电商平台-B2B生鲜的互联网思维 在互联网高速发展的今天,为我们的生活带来了众多便利.然而互联网从早期的萌芽状态到现在妇孺皆知,它的崛起速度远远超乎世人的想象.人们开始关注互联网并且研究它 ...
- 马蜂窝 iOS App 启动治理:回归用户体验
增长.活跃.留存是移动 App 的常见核心指标,直接反映一款 App 甚至一个互联网公司运行的健康程度和发展动能.启动流程的体验决定了用户的第一印象,在一定程度上影响了用户活跃度和留存率.因此,确保启 ...
- 科技风商务项目管理PPT模板
模板来源:http://ppt.dede58.com/peixunyanjiang/26267.html
- 面试官之问:知道你的接口“QPS”是多少吗?
前言: 原作:孤独烟.因修改不当之处欢迎指出! 大家好,我是小架架. 今天一大早就起来水文章了.这篇文章我个人感觉虽然含金量不是特别大,估计大家大概5分钟左右就能看完!到底是因为什么呢,因为平时干货文 ...
- Android框架Volley使用:Post请求实现
首先我们在项目中导入这个框架: implementation 'com.mcxiaoke.volley:library:1.0.19' 在AndroidManifest文件当中添加网络权限: < ...
- Springboot中定时器的简单使用
在定时器的类上添加注解: @Component@EnableAsync@EnableScheduling 一.普通的定时器: 每天15:10执行的定时器 @Scheduled(cron="0 ...
