RabbitMQ高级特性-消息确认与持久性博客

RabbitMQ 高级特性:消息确认与持久性

在 RabbitMQ 的基础使用中,我们已经知道了生产者、交换机、队列、消费者之间的基本流转关系。但真正落到业务系统里,仅仅"能发、能收"是不够的。消息可能在消费者处理失败时丢失,也可能在 RabbitMQ 服务重启后消失。

  1. 消息确认:解决"消息到达消费者后,是否真的被成功处理"的问题。
  2. 持久性:解决"RabbitMQ 服务异常或重启后,交换机、队列、消息是否还在"的问题。

1. 消息确认

1.1 消息确认机制

生产者把消息发送到 RabbitMQ 后,消费者拿到消息并处理时,可能出现两种结果:

  1. 消息处理成功。
  2. 消息处理异常。

如果 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 不会在投递后立即删除消息,而是等待消费者显式回复确认信号。

此时队列中的消息会出现两种状态:

  1. Ready:等待投递给消费者的消息。
  2. Unacked:已经投递给消费者,但还没有收到确认信号的消息。

如果 RabbitMQ 一直没有收到确认,并且对应消费者连接断开,RabbitMQ 会把这条消息重新放回队列,等待投递给下一个消费者。

这也是实际业务中更常用的方式,因为它能让 RabbitMQ 感知消费者是否真的处理成功。

1.2 手动确认方法

手动确认时,消费者可以根据业务处理结果选择确认、拒绝或者批量拒绝。RabbitMQ Java Client 主要提供三个方法。

basicAck:肯定确认
java 复制代码
channel.basicAck(long deliveryTag, boolean multiple);

表示消费者已经成功处理消息,RabbitMQ 可以删除该消息。

参数说明:

  1. deliveryTag:消息在当前 Channel 内的唯一标识,是单调递增的长整型值。
  2. multiple:是否批量确认。为 true 时,会确认所有小于等于当前 deliveryTag 的未确认消息;为 false 时,只确认当前这条消息。

注意:deliveryTag 是按 Channel 维护的,因此在哪个 Channel 收到消息,就要在哪个 Channel 上确认。

basicReject:拒绝单条消息
java 复制代码
channel.basicReject(long deliveryTag, boolean requeue);

表示消费者拒绝当前消息。

参数说明:

  1. deliveryTag:要拒绝的消息标识。
  2. requeue:是否重新入队。为 true 时,消息会重新进入队列,等待再次投递;为 false 时,消息会从队列中移除。

basicReject 一次只能拒绝一条消息。

basicNack:否定确认,支持批量拒绝
java 复制代码
channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);

basicNack 可以理解为增强版拒绝方法。它比 basicReject 多了一个 multiple 参数,可以批量拒绝消息。

参数说明:

  1. deliveryTag:消息标识。
  2. multiple:是否批量拒绝。为 true 时,拒绝当前 deliveryTag 之前所有未确认消息。
  3. 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=0Unacked=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);
        }
    }
}

这里的处理逻辑是:

  1. 业务处理成功:调用 basicAck 确认消息。
  2. 业务处理失败:调用 basicNack 拒绝消息。
  3. 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 的持久化主要分为三个部分:

  1. 交换机持久化。
  2. 队列持久化。
  3. 消息持久化。

这三者都很重要。只持久化其中一部分,通常无法完整保证消息在重启后仍然可用。

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

队列持久化和消息持久化要一起使用

持久化时要特别注意组合关系:

  1. 只设置队列持久化:RabbitMQ 重启后队列还在,但消息可能丢失。
  2. 只设置消息持久化:RabbitMQ 重启后队列没了,消息也没有地方存放。
  3. 队列和消息都持久化: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 节点宕机,仍然可能丢失少量消息。

针对这个问题,给出两个方向:

  1. 引入 RabbitMQ 仲裁队列,提高高可用能力。主节点异常时可以切换到从节点,降低单节点宕机造成消息丢失的风险。
  2. 在发送端引入事务机制或发送方确认机制,保证消息已经正确发送并存储到 RabbitMQ。

实际项目中,事务机制性能开销较大,使用得相对少。更常见的方案是发送方确认,也就是后续章节会讲到的 publisher confirm。

持久化的性能取舍

持久化会提高可靠性,但也会带来性能成本。写磁盘比写内存慢得多,如果所有消息都强制持久化,会影响 RabbitMQ 的整体吞吐量。

因此,是否持久化要结合业务重要程度做权衡:

  1. 订单、支付、库存等关键消息:建议开启持久化,并配合手动确认、发送方确认、死信队列等机制。
  2. 日志、埋点、实时统计等允许少量丢失的消息:可以根据吞吐量要求选择非持久化。

小结

RabbitMQ 的可靠性不是靠某一个配置单独完成的,而是由多个环节共同保证。

第一章的消息确认主要解决消费者侧问题:消息到达消费者后,只有真正处理成功,才应该通知 RabbitMQ 删除消息。自动确认简单但可能丢消息,手动确认更可靠,也更适合关键业务。

第二章的持久性主要解决 RabbitMQ 服务侧问题:交换机、队列和消息都需要持久化,RabbitMQ 重启后消息才有机会保留下来。但持久化也不是绝对可靠,还需要结合手动确认、仲裁队列、发送方确认等机制,才能构建更完整的可靠消息链路。

可以用一句话概括这两章的重点:

消费端靠手动确认防止"处理失败却删除消息",服务端靠持久化防止"服务重启后消息消失"。

相关推荐
霸道流氓气质16 小时前
Redisson 分布式集合详解:像用本地集合一样操作跨服务共享数据
分布式
HEADKON16 小时前
匹妥布替尼捷帕力Pirtobrutinib对比伊布替尼治疗套细胞淋巴瘤的缓解率更优
ruby
2603_9547083117 小时前
协调控制柜在微电网中的核心地位:数据枢纽、控制核心、安全屏障
分布式·安全·架构·能源·需求分析
淡漠的蓝精灵17 小时前
Pulsar 入门:云原生分布式消息流平台
分布式·其他·云原生
ai生成式引擎优化技术19 小时前
DLOS Kernel v1.0:面向分布式AI任务执行与Agent调度的统一运行时内核
人工智能·分布式
ai生成式引擎优化技术19 小时前
DLOS v0.7:面向分布式多智能体AI操作系统的自进化内核
人工智能·分布式
未若君雅裁19 小时前
RabbitMQ 消息可靠性:生产者确认、持久化、消费者ACK与幂等消费
分布式·微服务·rabbitmq
数据库小学妹19 小时前
分布式数据库架构演进:从集中式到分布式,三大路线一次讲清楚
数据库·分布式·数据库架构
juniperhan19 小时前
Flink 系列第25篇:Flink SQL 集成 Hive 实践:流批一体下的实时数仓利器
大数据·数据仓库·hive·分布式·sql·flink