【RabbitMQ 笔记】— 生产者确认
持久化虽然能解决 RabbitMQ 宕机数据丢失问题,但还有个问题就是,消息到底有没有正确到达服务器呢。如果不进行特殊配置,消息发送出去后,是没有任何操作来告诉生产者消息是否到达服务器。消息在到达服务器之前就丢失了,持久化也解决不了问题。因为消息都没有,谈何持久化。 RabbitMQ 为了针对这个问题,提供了两种解决方式:
- 通过事务机制实现
- 通过发送方确认(publisher confirm)机制实现
事务机制
RabbitMQ 中与事务相关的方法有三个:
- channel.txSelect:将当前信道设置为事务模式
- channel.txCommit:提交事务
- channel.txRollback:事务回滚
示例代码如下
Channel channel = null;
try(Connection connection = ConnectionUtil.getConnection()) {
channel = connection.createChannel();
channel.txSelect(); // 将信道设置为信道模式
channel.exchangeDeclare(EXCHANGE_NAME, "direct", false, false, null);
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
String message = "tx message " + new SimpleDateFormat("mm:ss");
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
int result = 1 / 0;
channel.txCommit();
} catch (IOException e) {
e.printStackTrace();
if (channel != null) {
channel.txRollback();
}
}
事务确实可以解决发送方和 RabbitMQ 之间消息确认的问题,只有消息正确到达 RabbitMQ,事务才能提交成功,否则便可在捕获异常后进行事务回滚,与此同时还可以进行消息重发。但是使用事务机制会大量消耗 RabbitMQ 的性能。所以,RabbitMQ 提供了另外一种方案,即生产者确认。
发送方确认
生产者将信道设置为 confirm(确认)模式,一旦进入确认模式,所有在该信道上发布的消息都会指派一个唯一的 ID。一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一 ID),这就使得消费者知晓消息已正确到达目的地。如果消息和队列是持久化的,那么确认消息会在消息落盘后发出。RabbitMQ 回传给生产者的确认消息中的 deliveryTag 包含了消息的序号。
发送方确认机制最大的好处在于它是异步的,一旦发布一条消息,生产者可以在等待确认消息的同时继续发送下一条消息。当消息最终达到之后,生产者可以通过回调来处理该确认消息。当然,如果 RabbitMQ 内部错误导致消息丢失,就会发送一条 nack(Basic.Nack) 命令。
消费者确认示例代码如下
try(Connection connection = ConnectionUtil.getConnection()) {
Channel channel = connection.createChannel();
channel.confirmSelect(); // 将信道设置为 publisher confirm 模式
String message = "tx message " + new SimpleDateFormat("mm:ss");
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
if (channel.waitForConfirms()) {
System.out.println("消息发送成功");
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
上面代码的确认实际上串行同步的方式,吞吐量并不是太高,但其实确认并不一定非要同步确认,所以针对这个问题,有两种优化方案:
- 批量确认
- 异步确认
批量确认
批量确认需要定期或定量,或者两者结合来调用 channel.waitForConfirms 来等待 RabbitMQ 的返回,相比于上面的普通确认,批量确认可以极大的提升 confirm 效率。但是也存在一些问题,比如 Basic.Nack 或超时的情况,客户端需要将者一批次的消息全部重发,这回带来明显的重复消息数量。并且当消息经常丢失时,批量 comfirm 的性能反而会降低。
异步确认
正是因为批量确认存在这些问题,所以我们重点看看异步确认方式。在客户端 Channel 接口中提供 addConfirmListener 方法中可以添加 ConfirmListener 这个回调接口。回调接口中包含两个方法:handleAck(对应 Basic.Ack) 和 handleNack(对应 Basic.Nack)。这两个方法中还包含一个 deliveryTag,也就是 publisher Confirm 模式下消息的唯一序列号。
异步 Confirm 示例代码如下
try(Connection connection = ConnectionUtil.getConnection()) {
final SortedSet<Long> unconfirmSet = new TreeSet<>();
Channel channel = connection.createChannel();
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("ack no: " + deliveryTag);
if (multiple) {
unconfirmSet.headSet(deliveryTag -1).clear();
} else {
unconfirmSet.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("nack no: " + deliveryTag);
if (multiple) {
unconfirmSet.headSet(deliveryTag -1).clear();
} else {
unconfirmSet.remove(deliveryTag);
}
// 这里需要有重发逻辑
}
});
// 发送消息
while (true) {
long nextPublishSeqNo = channel.getNextPublishSeqNo();
String message = "tx message " + new SimpleDateFormat("mm:ss");
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
unconfirmSet.add(nextPublishSeqNo);
TimeUnit.SECONDS.sleep(2);
}
} catch(Exception e) {
e.printStackTrace();
}
在实际项目应用中,还是推荐使用异步 Confirm 的方式。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了