4 发布确认
4.1 发布确认原理
生产者将信道设置成confirm
模式,一旦信道进入confirm
模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID
(从1
开始),一旦消息被投递到所有匹配的队列之后,broke
就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker
回传给生产者的确认消息中delivery-tag
域包含了确认消息的序列号,此外broker
也可以设置basic.ack
的multiple
域,表示到这个序列号之前的所有消息都已经得到了处理。confirm
模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ
因为自身内部错误导致消息丢失,就会发送一条nack
消息,生产者应用程序同样可以在回调方法中处理该nack
消息。
4.2 发布确认的策略
4.2.1 开启发布确认的方法
发布确认默认是没有开启的,如果要开启需要调用方法confirmSelect
。
4.2.2 单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)
这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点为发布速度特别的慢。
public static void publishMessageIndividually() throws Exception {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
// 开启发布确认
channel.confirmSelect();
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
// 服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean flag = channel.waitForConfirms();
if (!flag) {
System.out.println("[" + message + "]消息发送失败");
}
}
long end = System.currentTimeMillis();
System.out.println("单条确认方式-发布[" + MESSAGE_COUNT + "]个单独确认消息,耗时" + (end - begin) + "ms");
}
4.2.3 批量确认发布
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
public static void publishMessageBatch() throws Exception {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
// 该方法用于开启发布确认
channel.confirmSelect();
// 批大小
int batchSize = 100;
long begin = System.currentTimeMillis();
for (int i = 1; i <= MESSAGE_COUNT; i++) {
String message = "Hello-" + i;
channel.basicPublish("", queueName, null, message.getBytes());
if (i % batchSize == 0) {
channel.waitForConfirms();
}
}
if (MESSAGE_COUNT % batchSize != 0) {
channel.waitForConfirms();
}
long end = System.currentTimeMillis();
System.out.println("批量确认方式-发布[" + MESSAGE_COUNT + "]条消息,耗时[" + (end - begin) + "]ms");
}
4.2.4 异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,利用回调函数来达到消息可靠性传递。
public static void publishMessageAsync() throws Exception {
Channel channel = RabbitMQUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
// 开启发布确认
channel.confirmSelect();
// 线程安全有序的一个哈希表,适用于高并发场景
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
/*
* 添加一个异步确认的监听器
* 1.确认收到消息的回调
* 2.未收到消息的回调
*/
channel.addConfirmListener(
/*
* 确认收到消息回调
* 1.消息序列号
* 2.true 可以确认小于等于当前序列号的消息 false 确认当前序列号消息
*/
(number, multiple) -> {
if (multiple) {
outstandingConfirms.headMap(number, true).clear();
} else {
outstandingConfirms.remove(number);
}
},
(number, multiple) -> {
String message = outstandingConfirms.get(number);
System.out.println("发布的消息[" + message + "]未被确认,序列: [" + number + "]");
});
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = "Hello-" + i;
// channel.getNextPublishSeqNo() 获取下一个消息的序列号 通过序列号与消息体进行一个关联
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queueName, null, message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("异步确认方式-发布[" + MESSAGE_COUNT + "]个异步确认消息,耗时" + (end - begin) + "ms");
}
注:原始资料来源于尚硅谷,笔者学习过程中做了整理,加入了自己的实验代码