RabbitMQ 高级特性:消息确认与持久性
在 RabbitMQ 的基础使用中,我们已经知道了生产者、交换机、队列、消费者之间的基本流转关系。但真正落到业务系统里,仅仅"能发、能收"是不够的。消息可能在消费者处理失败时丢失,也可能在 RabbitMQ 服务重启后消失。
- 消息确认:解决"消息到达消费者后,是否真的被成功处理"的问题。
- 持久性:解决"RabbitMQ 服务异常或重启后,交换机、队列、消息是否还在"的问题。
1. 消息确认
1.1 消息确认机制
生产者把消息发送到 RabbitMQ 后,消费者拿到消息并处理时,可能出现两种结果:
- 消息处理成功。
- 消息处理异常。
如果 RabbitMQ 只要把消息推给消费者,就立刻把消息从队列中删除,那么第二种情况就会造成消息丢失。比如消费者刚收到消息,业务代码还没执行完就宕机了,此时 RabbitMQ 如果已经删除消息,这条消息就无法再次被消费。
为了解决这个问题,RabbitMQ 提供了消息确认机制,也就是 message acknowledgement。消费者订阅队列时,可以通过 autoAck 参数决定确认方式。
自动确认
当 autoAck=true 时,RabbitMQ 会认为消息只要投递给消费者,就已经消费成功,然后直接从内存或磁盘中删除。
这种方式代码简单,吞吐量也高,但可靠性较弱。它适合日志、统计等对消息丢失不敏感的场景。
Java Client 中消费者订阅队列的核心方法如下:
java
String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
示例代码:
java
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("接收到消息: " + new String(body));
}
};
channel.basicConsume(Constants.TOPIC_QUEUE_NAME1, true, consumer);
这里第二个参数传入 true,表示自动确认。也就是说,即使 handleDelivery 中后续业务处理失败,RabbitMQ 也不会再重新投递这条消息。
手动确认
当 autoAck=false 时,RabbitMQ 不会在投递后立即删除消息,而是等待消费者显式回复确认信号。
此时队列中的消息会出现两种状态:
Ready:等待投递给消费者的消息。Unacked:已经投递给消费者,但还没有收到确认信号的消息。
如果 RabbitMQ 一直没有收到确认,并且对应消费者连接断开,RabbitMQ 会把这条消息重新放回队列,等待投递给下一个消费者。
这也是实际业务中更常用的方式,因为它能让 RabbitMQ 感知消费者是否真的处理成功。
1.2 手动确认方法
手动确认时,消费者可以根据业务处理结果选择确认、拒绝或者批量拒绝。RabbitMQ Java Client 主要提供三个方法。
basicAck:肯定确认
java
channel.basicAck(long deliveryTag, boolean multiple);
表示消费者已经成功处理消息,RabbitMQ 可以删除该消息。
参数说明:
deliveryTag:消息在当前 Channel 内的唯一标识,是单调递增的长整型值。multiple:是否批量确认。为true时,会确认所有小于等于当前deliveryTag的未确认消息;为false时,只确认当前这条消息。
注意:deliveryTag 是按 Channel 维护的,因此在哪个 Channel 收到消息,就要在哪个 Channel 上确认。
basicReject:拒绝单条消息
java
channel.basicReject(long deliveryTag, boolean requeue);
表示消费者拒绝当前消息。
参数说明:
deliveryTag:要拒绝的消息标识。requeue:是否重新入队。为true时,消息会重新进入队列,等待再次投递;为false时,消息会从队列中移除。
basicReject 一次只能拒绝一条消息。
basicNack:否定确认,支持批量拒绝
java
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);
basicNack 可以理解为增强版拒绝方法。它比 basicReject 多了一个 multiple 参数,可以批量拒绝消息。
参数说明:
deliveryTag:消息标识。multiple:是否批量拒绝。为true时,拒绝当前deliveryTag之前所有未确认消息。requeue:是否重新入队。
业务中常见的写法是:处理成功就 basicAck,处理失败就 basicNack,并根据失败类型决定是否重新入队。
1.3 Spring Boot 中的消息确认示例
Spring AMQP 对确认机制做了一层封装,常见确认模式有三种:
java
public enum AcknowledgeMode {
NONE,
MANUAL,
AUTO;
}
AcknowledgeMode.NONE
NONE 表示不启用消费者确认机制。消息一旦投递给消费者,RabbitMQ 就会认为消费成功并删除消息。
配置示例:
yaml
spring:
rabbitmq:
addresses: amqp://user:password@host:port/vhost
listener:
simple:
acknowledge-mode: none
先准备交换机、队列和绑定关系:
java
public class Constant {
public static final String ACK_EXCHANGE_NAME = "ack_exchange";
public static final String ACK_QUEUE = "ack_queue";
}
java
@Configuration
public class AckConfig {
@Bean("ackExchange")
public Exchange ackExchange() {
return ExchangeBuilder
.topicExchange(Constant.ACK_EXCHANGE_NAME)
.durable(true)
.build();
}
@Bean("ackQueue")
public Queue ackQueue() {
return QueueBuilder
.durable(Constant.ACK_QUEUE)
.build();
}
@Bean("ackBinding")
public Binding ackBinding(@Qualifier("ackExchange") Exchange exchange,
@Qualifier("ackQueue") Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("ack")
.noargs();
}
}
生产者发送消息:
java
@RestController
@RequestMapping("/producer")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("/ack")
public String ack() {
rabbitTemplate.convertAndSend(
Constant.ACK_EXCHANGE_NAME,
"ack",
"consumer ack test..."
);
return "发送成功!";
}
}
消费者模拟异常:
java
@Component
public class AckQueueListener {
@RabbitListener(queues = Constant.ACK_QUEUE)
public void listenerQueue(Message message, Channel channel) throws Exception {
System.out.printf("接收到消息: %s, deliveryTag: %d%n",
new String(message.getBody(), StandardCharsets.UTF_8),
message.getMessageProperties().getDeliveryTag());
int num = 3 / 0;
System.out.println("处理完成");
}
}
在 NONE 模式下,即使消费者抛出异常,消息也已经被 RabbitMQ 视为消费成功并移除。因此可以看到 Ready=0,Unacked=0,但业务实际并没有处理完成。
AcknowledgeMode.AUTO
AUTO 是 Spring AMQP 的默认确认模式。消费者方法正常执行完成时,Spring 会自动确认消息;如果方法抛出异常,则不会确认消息。
配置示例:
yaml
spring:
rabbitmq:
addresses: amqp://user:password@host:port/vhost
listener:
simple:
acknowledge-mode: auto
如果继续使用上面带异常的消费者代码,消息会被不断重新投递。日志中可以看到 deliveryTag 持续递增:
text
接收到消息: consumer ack test..., deliveryTag: 1
接收到消息: consumer ack test..., deliveryTag: 2
接收到消息: consumer ack test..., deliveryTag: 3
这种模式比 NONE 更可靠,因为异常时不会直接删除消息。但如果业务代码一直异常,又没有配合重试次数、死信队列等机制,就可能造成消息反复重投或积压。
AcknowledgeMode.MANUAL
MANUAL 是手动确认模式。消费者需要自己决定什么时候确认、什么时候拒绝。
配置示例:
yaml
spring:
rabbitmq:
addresses: amqp://user:password@host:port/vhost
listener:
simple:
acknowledge-mode: manual
消费者代码:
java
@Component
public class AckQueueListener {
@RabbitListener(queues = Constant.ACK_QUEUE)
public void listenerQueue(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
System.out.printf("接收到消息: %s, deliveryTag: %d%n",
new String(message.getBody(), StandardCharsets.UTF_8),
deliveryTag);
System.out.println("处理业务逻辑");
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
channel.basicNack(deliveryTag, false, true);
}
}
}
这里的处理逻辑是:
- 业务处理成功:调用
basicAck确认消息。 - 业务处理失败:调用
basicNack拒绝消息。 requeue=true:消息重新进入队列,等待下一次投递。
如果为了测试异常,把业务代码改成下面这样:
java
System.out.println("处理业务逻辑");
int num = 3 / 0;
channel.basicAck(deliveryTag, false);
异常会进入 catch,然后执行:
java
channel.basicNack(deliveryTag, false, true);
由于 requeue=true,消息会不断重新入队并再次投递,控制台会看到类似输出:
text
接收到消息: consumer ack test..., deliveryTag: 1
处理业务逻辑
接收到消息: consumer ack test..., deliveryTag: 2
处理业务逻辑
接收到消息: consumer ack test..., deliveryTag: 3
处理业务逻辑
手动确认模式最灵活,也最适合可靠性要求高的业务。但实际项目中不要让失败消息无限重试,通常要配合重试次数、死信队列、告警日志等机制。
2. 持久性
刚刚解决的是消费者处理消息时如何避免丢失。接下来要解决另一个问题:如果 RabbitMQ 服务停止、重启或异常崩溃,交换机、队列和消息是否还能保留下来?
默认情况下,如果没有进行持久化配置,RabbitMQ 在退出或崩溃后可能会忽略原来的队列和消息。RabbitMQ 的持久化主要分为三个部分:
- 交换机持久化。
- 队列持久化。
- 消息持久化。
这三者都很重要。只持久化其中一部分,通常无法完整保证消息在重启后仍然可用。
2.1 交换机持久化
交换机持久化通过声明交换机时设置 durable=true 实现。
java
ExchangeBuilder
.topicExchange(Constant.ACK_EXCHANGE_NAME)
.durable(true)
.build();
交换机持久化后,RabbitMQ 会保存交换机的元数据。即使 RabbitMQ 服务重启,交换机也会恢复,不需要应用程序重新创建。
如果交换机没有持久化,RabbitMQ 重启后交换机元数据会丢失。对于长期使用的业务交换机,建议都声明为持久化。
2.2 队列持久化
队列持久化通过声明队列时设置 durable=true 实现。
java
QueueBuilder
.durable(Constant.ACK_QUEUE)
.build();
Spring AMQP 中 QueueBuilder.durable(name) 默认就是创建持久化队列。源码逻辑大致如下:
java
public static QueueBuilder durable(String name) {
return new QueueBuilder(name).setDurable();
}
private QueueBuilder setDurable() {
this.durable = true;
return this;
}
如果要创建非持久化队列,可以使用:
java
QueueBuilder
.nonDurable(Constant.ACK_QUEUE)
.build();
需要注意的是,队列持久化只能保证队列本身的元数据不会因为 RabbitMQ 重启而丢失,不能保证队列里的消息一定还在。
原因很简单:队列还在,不代表队列中的消息被持久化了。要让消息也在重启后保留,还必须设置消息持久化。
2.3 消息持久化
消息持久化需要把消息的投递模式设置为持久化,也就是 MessageDeliveryMode.PERSISTENT。
Spring AMQP 中的枚举如下:
java
public enum MessageDeliveryMode {
NON_PERSISTENT,
PERSISTENT
}
如果使用 RabbitMQ Java Client,可以在发送消息时设置 MessageProperties.PERSISTENT_TEXT_PLAIN:
java
// 非持久化消息
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
// 持久化消息
channel.basicPublish(
"",
QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
msg.getBytes()
);
PERSISTENT_TEXT_PLAIN 本质上是把 deliveryMode 设置为 2:
java
public static final BasicProperties PERSISTENT_TEXT_PLAIN =
new BasicProperties(
"text/plain",
null,
null,
2,
0,
null,
null,
null,
null,
null,
null,
null,
null,
null
);
如果使用 RabbitTemplate 发送持久化消息,可以手动构造 Message:
java
String text = "This is a persistent message";
MessageProperties messageProperties = new MessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
Message message = new Message(
text.getBytes(StandardCharsets.UTF_8),
messageProperties
);
rabbitTemplate.convertAndSend(
Constant.ACK_EXCHANGE_NAME,
"ack",
message
);
消费端打印 MessageProperties 时,也可以观察消息是否是持久化投递,例如:
text
receivedDeliveryMode=PERSISTENT
队列持久化和消息持久化要一起使用
持久化时要特别注意组合关系:
- 只设置队列持久化:RabbitMQ 重启后队列还在,但消息可能丢失。
- 只设置消息持久化:RabbitMQ 重启后队列没了,消息也没有地方存放。
- 队列和消息都持久化:RabbitMQ 重启后,消息才有机会继续保留在队列中。
所以,在可靠性要求高的场景中,通常至少要做到:
java
@Bean
public Exchange ackExchange() {
return ExchangeBuilder
.topicExchange(Constant.ACK_EXCHANGE_NAME)
.durable(true)
.build();
}
@Bean
public Queue ackQueue() {
return QueueBuilder
.durable(Constant.ACK_QUEUE)
.build();
}
public void sendPersistentMessage(String text) {
MessageProperties properties = new MessageProperties();
properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
Message message = new Message(
text.getBytes(StandardCharsets.UTF_8),
properties
);
rabbitTemplate.convertAndSend(
Constant.ACK_EXCHANGE_NAME,
"ack",
message
);
}
也就是:交换机持久化、队列持久化、消息持久化一起配置。
持久化不是百分之百不丢
把交换机、队列、消息都设置为持久化之后,是不是就能百分之百保证数据不丢失?
答案是否定的。
第一种情况和消费者确认有关。如果消费者使用自动确认,RabbitMQ 把消息投递出去后就删除消息,但消费者还没处理完就宕机,消息仍然会丢失。因此,关键业务需要结合第一章的手动确认机制。
第二种情况和落盘时机有关。持久化消息到达 RabbitMQ 后,并不代表每一条消息都会立刻同步刷盘。RabbitMQ 可能先把消息写入操作系统缓存,再由系统刷入磁盘。如果在这个极短的时间窗口里 RabbitMQ 节点宕机,仍然可能丢失少量消息。
针对这个问题,给出两个方向:
- 引入 RabbitMQ 仲裁队列,提高高可用能力。主节点异常时可以切换到从节点,降低单节点宕机造成消息丢失的风险。
- 在发送端引入事务机制或发送方确认机制,保证消息已经正确发送并存储到 RabbitMQ。
实际项目中,事务机制性能开销较大,使用得相对少。更常见的方案是发送方确认,也就是后续章节会讲到的 publisher confirm。
持久化的性能取舍
持久化会提高可靠性,但也会带来性能成本。写磁盘比写内存慢得多,如果所有消息都强制持久化,会影响 RabbitMQ 的整体吞吐量。
因此,是否持久化要结合业务重要程度做权衡:
- 订单、支付、库存等关键消息:建议开启持久化,并配合手动确认、发送方确认、死信队列等机制。
- 日志、埋点、实时统计等允许少量丢失的消息:可以根据吞吐量要求选择非持久化。
小结
RabbitMQ 的可靠性不是靠某一个配置单独完成的,而是由多个环节共同保证。
第一章的消息确认主要解决消费者侧问题:消息到达消费者后,只有真正处理成功,才应该通知 RabbitMQ 删除消息。自动确认简单但可能丢消息,手动确认更可靠,也更适合关键业务。
第二章的持久性主要解决 RabbitMQ 服务侧问题:交换机、队列和消息都需要持久化,RabbitMQ 重启后消息才有机会保留下来。但持久化也不是绝对可靠,还需要结合手动确认、仲裁队列、发送方确认等机制,才能构建更完整的可靠消息链路。
可以用一句话概括这两章的重点:
消费端靠手动确认防止"处理失败却删除消息",服务端靠持久化防止"服务重启后消息消失"。