文章目录
- [1. Publisher Confirms(发布确认)](#1. Publisher Confirms(发布确认))
- [2. Publishing Messages Individually(单独确认)](#2. Publishing Messages Individually(单独确认))
- [3. Publishing Messages in Batches(批量确认)](#3. Publishing Messages in Batches(批量确认))
- [4. Handling Publisher Confirms Asynchronously(异步确认)](#4. Handling Publisher Confirms Asynchronously(异步确认))
- [5. 三种策略完整代码](#5. 三种策略完整代码)
1. Publisher Confirms(发布确认)
作为消息中间件,都会面临消息丢失的问题。
消息丢失大概分为三种情况:
- 1、生产者问题。因为应用程序故障,网络抖动等各种原因,生产者没有成功向 broker 发送消息。
- 2、消息中间件自身问题。生产者成功发送给了 Broker,但是 Broker 没有把消息保存好,导致消息丢失。
- 3、消费者问题。Broker 发送消息到消费者,消费者在消费消息时,因为没有处理好,导致 broker 将消费失败的消息从队列中删除了。
如下图所示:

RabbitMQ 也对上述问题给出了相应的解决方案。问题 2 可以通过持久化机制,问题 3 可以采用消息应答机制。针对问题1,可以采用发布确认(Publisher Confirms)机制实现。
发布确认属于 RabbitMQ 的七大工作模式之一,它是解决生产者的问题。
生产者将信道设置成 confirm(确认)模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出。broker 回传给生产者的确认消息中 deliveryTag 包含了确认消息的序号,此外 broker 也可以设置 channel.basicAck 方法中的 multiple 参数,表示到这个序号之前的所有消息都已经得到了处理。
如下所示:

发送方确认机制最大的好处在于它是异步的,生产者可以同时发布消息和等待信道返回确认消息。
- 1、当消息最终得到确认之后,生产者可以通过回调方法来处理该确认消息。
- 2、如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack(Basic.Nack)命令,生产者同样可以在回调方法中处理该 nack 命令。
使用发送确认机制,必须要信道设置成 confirm(确认)模式。
发布确认是 AMQP 0.9.1 协议的扩展,默认情况下它不会被启用。生产者通过 channel.confirmSelect() 将信道设置为 confirm 模式。
java
Channel channel = connection.createChannel();
channel.confirmSelect();
发布确认有 3 种策略,接下来我们来学习这三种策略,每种策略都创建一个新的队列。
java
// publisher confirms
public static final String PUBLISHER_CONFIRMS_QUEUE1 = "publisher.confirms.queue1";
public static final String PUBLISHER_CONFIRMS_QUEUE2 = "publisher.confirms.queue2";
public static final String PUBLISHER_CONFIRMS_QUEUE3 = "publisher.confirms.queue3";
2. Publishing Messages Individually(单独确认)
所谓单独确认,就是生产者每发送一条消息,就需要等待 Broker 的确认。
代码如下所示:
java
/**
* 单独确认
*/
private static void publishingMessagesIndividually() throws IOException, TimeoutException, InterruptedException {
try(Connection connection = createConnection()) {
// 1. 开启信道
Channel channel = connection.createChannel();
// 2. 设置信道为confirm模式
channel.confirmSelect();
// 3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE1, true, false, false, null);
// 4. 发生消息并等待确认
long startTime = System.currentTimeMillis();
for (int i = 0; i <MESSAGE_COUNT; i++) {
// 发生消息
String message = "Hello publisher confirms " + i;
channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE1, null, message.getBytes());
// 等待确认
channel.waitForConfirmsOrDie(5000); // 等待5秒
}
long endTime = System.currentTimeMillis();
System.out.printf("单独确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
}
}
运行结果如下:

并且可以看到队列中也有 200 条消息:

消息内容如下:

但是可以发现,发送 200 条消息,耗时很长,用了 3518 ms。
观察上面代码,会发现这种策略是每发送一条消息后就调用 channel.waitForConfirmsOrDie 方法,之后等待服务端的确认,这实际上是一种串行同步等待的方式。尤其对于持久化的消息来说,需要等待消息确认存储在磁盘之后才会返回(调用 Linux 内核的 fsync 方法)。
但是发布确认机制是支持异步的,可以一边发送消息,一边等待消息确认。
由此进行了改进,接下来看另外一种策略:
- Publishing Messages in Batches(批量确认):每发送一批消息后,调用 channel.waitForConfirms 方法,等待服务器的确认返回。
3. Publishing Messages in Batches(批量确认)
代码如下所示:
java
/**
* 批量确认
*/
private static void publishingMessagesInBatches() throws IOException, TimeoutException, InterruptedException {
try(Connection connection = createConnection()) {
// 1. 开启信道
Channel channel = connection.createChannel();
// 2. 设置信道为confirm模式
channel.confirmSelect();
// 3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE2, true, false, false, null);
// 4. 发生消息, 并进行确认
long startTime = System.currentTimeMillis();
int batchSize = 100; // 设置批量处理的大小(每100条就确认一次)
int outStandingMessageCount = 0; // 计数器
for (int i = 0; i <MESSAGE_COUNT; i++) {
// 发生消息
String message = "Hello publisher confirms " + i;
channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE2, null, message.getBytes());
outStandingMessageCount++;
if (outStandingMessageCount == batchSize) {
// 确认
channel.waitForConfirmsOrDie(5000); // 等待5秒
outStandingMessageCount = 0; // 重置计数器
}
}
if(outStandingMessageCount > 0) {
channel.waitForConfirmsOrDie(5000);
}
long endTime = System.currentTimeMillis();
System.out.printf("批量确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
}
}
运行结果如下:

可以观察到,性能提高了很多。
相比于单独确认策略,批量确认极大地提升了 confirm 的效率,缺点是出现 Basic.Nack 或者超时时,我们不清楚具体哪条消息出了问题。客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量。
当消息经常丢失时,批量确认的性能应该是不升反降的。
由此进行了改进,接下来看最后一种策略:
- Handling Publisher Confirms Asynchronously(异步确认):提供一个回调方法,服务端确认了一条或者多条消息后客户端会回这个方法进行处理。
4. Handling Publisher Confirms Asynchronously(异步确认)
异步 confirm 方法的编程实现最为复杂。Channel 接口提供了一个方法 addConfirmListener,这个方法可以添加 ConfirmListener 回调接口。
ConfirmListener 接口中包含两个方法:handleAck(long deliveryTag, boolean multiple) 和 handleNack(long deliveryTag, boolean multiple),分别对应处理 RabbitMQ 发送给生产者的 ack 和 nack。
deliveryTag 表示发送消息的序号。multiple 表示是否批量确认。
我们需要为每一个 Channel 维护一个已发送消息的序号集合。当收到 RabbitMQ 的 confirm 回调时,从集合中删除对应的消息。当 Channel 开启 confirm 模式后,channel 上发送消息都会附带一个从 1 开始递增的 deliveryTag 序号。我们可以使用 SortedSet 的有序性来维护这个已发消息的集合。
- 1、当收到 ack 时,从序列中删除该消息的序号。如果为批量确认消息,表示小于等于当前序号 deliveryTag 的消息都收到了,则清除对应集合。
- 2、当收到 nack 时,处理逻辑类似,不过需要结合具体的业务情况,进行消息重发等操作。
代码如下所示:
java
/**
* 异步确认
*/
private static void handlingPublisherConfirmsAsynchronously() throws IOException, TimeoutException, InterruptedException {
try(Connection connection = createConnection()) {
// 1. 开启信道
Channel channel = connection.createChannel();
// 2. 设置信道为confirm模式
channel.confirmSelect();
// 3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE3, true, false, false, null);
// 4. 监听confirm
long startTime = System.currentTimeMillis();
// 有序集合, 元素按照⾃然顺序进⾏排序, 集合中存储的是未确认的消息ID
SortedSet<Long> confirmSeqNo = Collections.synchronizedSortedSet(new TreeSet<>());
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
// 批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
confirmSeqNo.headSet(deliveryTag + 1).clear();
}
else {
//单条确认:将当前的deliveryTag从集合中移除
confirmSeqNo.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
// 批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
confirmSeqNo.headSet(deliveryTag + 1).clear();
}
else {
//单条确认:将当前的deliveryTag从集合中移除
confirmSeqNo.remove(deliveryTag);
}
//如果处理失败,这⾥需要添加处理消息重发的场景, 此处代码省略
//TODO......
}
});
// 5. 发生消息
// 循环发送消息
for (int i = 0; i <MESSAGE_COUNT; i++) {
String message = "Hello publisher confirms " + i;
// 得到下次发送消息的序号, 从1开始
long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE3, null, message.getBytes());
// 将序号存⼊集合中
confirmSeqNo.add(seqNo);
}
// 消息确认完毕
while (!confirmSeqNo.isEmpty()) {
Thread.sleep(100); // 休眠100毫秒
}
long endTime = System.currentTimeMillis();
System.out.printf("异步确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
}
}
运行结果如下:把消息条数改为 10000 条,只对比批量确认即可。

可以看到,异步确认策略下,消息量越大,那么性能就越好。
5. 三种策略完整代码
代码如下所示:
java
package publisher.confirms;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import constant.Constants;
import java.io.IOException;
import java.util.Collections;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.TimeoutException;
public class PublisherConfirms {
private static final Integer MESSAGE_COUNT = 20000; // 测试的消息数
static Connection createConnection() throws IOException, TimeoutException {
// 建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(Constants.HOST); // MQ所在的服务器地址
factory.setPort(Constants.PORT); // 端口号
factory.setUsername(Constants.USERNAME); // 账号
factory.setPassword(Constants.PASSWORD); // 密码
factory.setVirtualHost(Constants.VIRTUAL_HOST); // 虚拟主机
return factory.newConnection();
}
public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
// 策略一:单独确认
publishingMessagesIndividually();
// 策略二:批量确认
publishingMessagesInBatches();
// 策略一:异步确认
handlingPublisherConfirmsAsynchronously();
}
/**
* 异步确认
*/
private static void handlingPublisherConfirmsAsynchronously() throws IOException, TimeoutException, InterruptedException {
try(Connection connection = createConnection()) {
// 1. 开启信道
Channel channel = connection.createChannel();
// 2. 设置信道为confirm模式
channel.confirmSelect();
// 3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE3, true, false, false, null);
// 4. 监听confirm
long startTime = System.currentTimeMillis();
// 有序集合, 元素按照⾃然顺序进⾏排序, 集合中存储的是未确认的消息ID
SortedSet<Long> confirmSeqNo = Collections.synchronizedSortedSet(new TreeSet<>());
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
// 批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
confirmSeqNo.headSet(deliveryTag + 1).clear();
}
else {
//单条确认:将当前的deliveryTag从集合中移除
confirmSeqNo.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
// 批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
confirmSeqNo.headSet(deliveryTag + 1).clear();
}
else {
//单条确认:将当前的deliveryTag从集合中移除
confirmSeqNo.remove(deliveryTag);
}
//如果处理失败,这⾥需要添加处理消息重发的场景, 此处代码省略
//TODO......
}
});
// 5. 发生消息
// 循环发送消息
for (int i = 0; i <MESSAGE_COUNT; i++) {
String message = "Hello publisher confirms " + i;
// 得到下次发送消息的序号, 从1开始
long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE3, null, message.getBytes());
// 将序号存⼊集合中
confirmSeqNo.add(seqNo);
}
// 消息确认完毕
while (!confirmSeqNo.isEmpty()) {
Thread.sleep(100); // 休眠100毫秒
}
long endTime = System.currentTimeMillis();
System.out.printf("异步确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
}
}
/**
* 批量确认
*/
private static void publishingMessagesInBatches() throws IOException, TimeoutException, InterruptedException {
try(Connection connection = createConnection()) {
// 1. 开启信道
Channel channel = connection.createChannel();
// 2. 设置信道为confirm模式
channel.confirmSelect();
// 3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE2, true, false, false, null);
// 4. 发生消息, 并进行确认
long startTime = System.currentTimeMillis();
int batchSize = 100; // 设置批量处理的大小(每100条就确认一次)
int outStandingMessageCount = 0; // 计数器
for (int i = 0; i <MESSAGE_COUNT; i++) {
// 发生消息
String message = "Hello publisher confirms " + i;
channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE2, null, message.getBytes());
outStandingMessageCount++;
// 批量确认消息
if (outStandingMessageCount == batchSize) {
channel.waitForConfirmsOrDie(5000); // 等待5秒
outStandingMessageCount = 0; // 重置计数器
}
}
// 消息发送完, 还有未确认的消息, 进行确认
if(outStandingMessageCount > 0) {
channel.waitForConfirmsOrDie(5000);
}
long endTime = System.currentTimeMillis();
System.out.printf("批量确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
}
}
/**
* 单独确认
*/
private static void publishingMessagesIndividually() throws IOException, TimeoutException, InterruptedException {
try(Connection connection = createConnection()) {
// 1. 开启信道
Channel channel = connection.createChannel();
// 2. 设置信道为confirm模式
channel.confirmSelect();
// 3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE1, true, false, false, null);
// 4. 发生消息, 等待确认
long startTime = System.currentTimeMillis();
for (int i = 0; i <MESSAGE_COUNT; i++) {
// 发生消息
String message = "Hello publisher confirms " + i;
channel.basicPublish("", Constants.PUBLISHER_CONFIRMS_QUEUE1, null, message.getBytes());
// 等待确认
channel.waitForConfirmsOrDie(5000); // 等待5秒
}
long endTime = System.currentTimeMillis();
System.out.printf("单独确认策略, 消息条数: %d, 耗时: %d ms\n", MESSAGE_COUNT, endTime - startTime);
}
}
}
把消息条数改为 20000 条,运行结果如下:

可以看到,消息数越多,异步确认的优势越明显。