[RabbitMQ高级特性] 消息可靠投递:从消息丢失问题到持久化与发送方确认解决方案

开头

我学习 RabbitMQ 高级特性时,一开始以为只要把队列设置成 durable(true),消息就不会丢了。后来在测试 RabbitMQ 重启、交换机写错、路由键写错这些场景时才发现:队列持久化、消息持久化、发送方确认解决的是不同阶段的问题

这篇文章主要解决两个问题:

  1. RabbitMQ 重启后,为什么有些消息还会丢?
  2. 生产者发送消息后,如何知道消息真的到达了 RabbitMQ?

一、先介绍背景:消息到底可能丢在哪?

我最开始遇到的场景是:生产者调用接口提示"发送成功",但消费者没有收到消息。排查后发现,"发送成功"并不等于消息已经可靠进入队列。

RabbitMQ 一条消息大致会经历:
#mermaid-svg-rVaneqEgNaCPXpNw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rVaneqEgNaCPXpNw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rVaneqEgNaCPXpNw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rVaneqEgNaCPXpNw .error-icon{fill:#552222;}#mermaid-svg-rVaneqEgNaCPXpNw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rVaneqEgNaCPXpNw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rVaneqEgNaCPXpNw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rVaneqEgNaCPXpNw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rVaneqEgNaCPXpNw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rVaneqEgNaCPXpNw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rVaneqEgNaCPXpNw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rVaneqEgNaCPXpNw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rVaneqEgNaCPXpNw .marker.cross{stroke:#333333;}#mermaid-svg-rVaneqEgNaCPXpNw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rVaneqEgNaCPXpNw p{margin:0;}#mermaid-svg-rVaneqEgNaCPXpNw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rVaneqEgNaCPXpNw .cluster-label text{fill:#333;}#mermaid-svg-rVaneqEgNaCPXpNw .cluster-label span{color:#333;}#mermaid-svg-rVaneqEgNaCPXpNw .cluster-label span p{background-color:transparent;}#mermaid-svg-rVaneqEgNaCPXpNw .label text,#mermaid-svg-rVaneqEgNaCPXpNw span{fill:#333;color:#333;}#mermaid-svg-rVaneqEgNaCPXpNw .node rect,#mermaid-svg-rVaneqEgNaCPXpNw .node circle,#mermaid-svg-rVaneqEgNaCPXpNw .node ellipse,#mermaid-svg-rVaneqEgNaCPXpNw .node polygon,#mermaid-svg-rVaneqEgNaCPXpNw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rVaneqEgNaCPXpNw .rough-node .label text,#mermaid-svg-rVaneqEgNaCPXpNw .node .label text,#mermaid-svg-rVaneqEgNaCPXpNw .image-shape .label,#mermaid-svg-rVaneqEgNaCPXpNw .icon-shape .label{text-anchor:middle;}#mermaid-svg-rVaneqEgNaCPXpNw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rVaneqEgNaCPXpNw .rough-node .label,#mermaid-svg-rVaneqEgNaCPXpNw .node .label,#mermaid-svg-rVaneqEgNaCPXpNw .image-shape .label,#mermaid-svg-rVaneqEgNaCPXpNw .icon-shape .label{text-align:center;}#mermaid-svg-rVaneqEgNaCPXpNw .node.clickable{cursor:pointer;}#mermaid-svg-rVaneqEgNaCPXpNw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rVaneqEgNaCPXpNw .arrowheadPath{fill:#333333;}#mermaid-svg-rVaneqEgNaCPXpNw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rVaneqEgNaCPXpNw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rVaneqEgNaCPXpNw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rVaneqEgNaCPXpNw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rVaneqEgNaCPXpNw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rVaneqEgNaCPXpNw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rVaneqEgNaCPXpNw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rVaneqEgNaCPXpNw .cluster text{fill:#333;}#mermaid-svg-rVaneqEgNaCPXpNw .cluster span{color:#333;}#mermaid-svg-rVaneqEgNaCPXpNw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rVaneqEgNaCPXpNw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rVaneqEgNaCPXpNw rect.text{fill:none;stroke-width:0;}#mermaid-svg-rVaneqEgNaCPXpNw .icon-shape,#mermaid-svg-rVaneqEgNaCPXpNw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rVaneqEgNaCPXpNw .icon-shape p,#mermaid-svg-rVaneqEgNaCPXpNw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rVaneqEgNaCPXpNw .icon-shape .label rect,#mermaid-svg-rVaneqEgNaCPXpNw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rVaneqEgNaCPXpNw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rVaneqEgNaCPXpNw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rVaneqEgNaCPXpNw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送失败
路由失败
服务重启丢失
消费异常
生产者 Producer
交换机 Exchange
队列 Queue
消费者 Consumer
Confirm 确认
Return 退回
持久化
手动 Ack

这张图的关键是:

持久化 主要解决消息进入 RabbitMQ 后,服务异常重启导致的数据丢失;

发送方确认 主要解决生产者不知道消息有没有到达 RabbitMQ 的问题。


二、用一个具体案例说明问题

我做了一个简单接口:

java 复制代码
rabbitTemplate.convertAndSend("confirm_exchange", "confirm", "confirm test...");

我一开始只关注消费者有没有打印日志,后来故意把交换机名字写错:

java 复制代码
rabbitTemplate.convertAndSend("confirm_exchange1", "confirm", "confirm test...");

结果消费者肯定收不到消息,但接口本身不一定能直观看出业务层面的可靠失败。

这时问题就来了:

场景 只做持久化能解决吗 需要什么机制
RabbitMQ 重启后队列丢失 能,队列持久化 Queue durable
RabbitMQ 重启后消息丢失 能,消息持久化 Message persistent
交换机不存在 不能 ConfirmCallback
路由键匹配不到队列 不能 ReturnCallback
消费者处理失败 不能 手动 Ack / Nack

三、分析问题出现的原因

1. 队列持久化不等于消息持久化

这里很容易误解。我一开始以为:

java 复制代码
QueueBuilder.durable("ack_queue").build();

通过durable建立持久化的队列,就能保证消息不丢。

实际上它只保证:RabbitMQ 重启后,队列这个元数据还在

如果消息本身不是持久化的,RabbitMQ 重启后消息仍然可能丢失。

2. 消息没到 RabbitMQ,持久化也没用

持久化的前提是消息已经进入 RabbitMQ。

如果生产者发送过程中网络异常、交换机不存在,消息根本没到 Broker,就谈不上落盘。

所以还需要发送方确认机制:
Queue Exchange Producer Queue Exchange Producer #mermaid-svg-SqLdED3qxr4WB4eJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SqLdED3qxr4WB4eJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SqLdED3qxr4WB4eJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SqLdED3qxr4WB4eJ .error-icon{fill:#552222;}#mermaid-svg-SqLdED3qxr4WB4eJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SqLdED3qxr4WB4eJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SqLdED3qxr4WB4eJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SqLdED3qxr4WB4eJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SqLdED3qxr4WB4eJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SqLdED3qxr4WB4eJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SqLdED3qxr4WB4eJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SqLdED3qxr4WB4eJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SqLdED3qxr4WB4eJ .marker.cross{stroke:#333333;}#mermaid-svg-SqLdED3qxr4WB4eJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SqLdED3qxr4WB4eJ p{margin:0;}#mermaid-svg-SqLdED3qxr4WB4eJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SqLdED3qxr4WB4eJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-SqLdED3qxr4WB4eJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SqLdED3qxr4WB4eJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-SqLdED3qxr4WB4eJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-SqLdED3qxr4WB4eJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-SqLdED3qxr4WB4eJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-SqLdED3qxr4WB4eJ .sequenceNumber{fill:white;}#mermaid-svg-SqLdED3qxr4WB4eJ #sequencenumber{fill:#333;}#mermaid-svg-SqLdED3qxr4WB4eJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-SqLdED3qxr4WB4eJ .messageText{fill:#333;stroke:none;}#mermaid-svg-SqLdED3qxr4WB4eJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SqLdED3qxr4WB4eJ .labelText,#mermaid-svg-SqLdED3qxr4WB4eJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-SqLdED3qxr4WB4eJ .loopText,#mermaid-svg-SqLdED3qxr4WB4eJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-SqLdED3qxr4WB4eJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SqLdED3qxr4WB4eJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-SqLdED3qxr4WB4eJ .noteText,#mermaid-svg-SqLdED3qxr4WB4eJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-SqLdED3qxr4WB4eJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SqLdED3qxr4WB4eJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SqLdED3qxr4WB4eJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SqLdED3qxr4WB4eJ .actorPopupMenu{position:absolute;}#mermaid-svg-SqLdED3qxr4WB4eJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-SqLdED3qxr4WB4eJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SqLdED3qxr4WB4eJ .actor-man circle,#mermaid-svg-SqLdED3qxr4WB4eJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-SqLdED3qxr4WB4eJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt路由成功路由失败 发送消息ConfirmCallback ack=true/false根据 routingKey 路由入队成功ReturnCallback 返回消息

关键节点解释:

  1. ConfirmCallback 关注消息有没有到达交换机。
  2. ReturnCallback 关注消息到达交换机后,有没有路由到队列。
  3. 两者配合,才能覆盖生产者到队列前的主要异常。

四、一步步给出解决方案

1. 交换机持久化

交换机持久化通过 durable(true) 设置。

java 复制代码
@Bean("presExchange")  
public DirectExchange presExchange(){  
    return ExchangeBuilder.directExchange(Constants.PRES_EXCHANGE)  
            .durable(false)  
            .build();  
}

它解决的问题是:RabbitMQ 重启后,交换机不会消失。


2. 队列持久化

java 复制代码
@Bean("presQueue")  
public Queue presQueue(){  
    return QueueBuilder  
            .durable(Constants.PRES_QUEUE)  
            .build();  
}

如果不设置队列持久化,RabbitMQ 重启后队列会被删除,消息自然也没有地方保存。


3. 建立绑定关系

java 复制代码
@Bean("presBinding")  
public Binding presBinding(@Qualifier("presQueue") Queue queue, @Qualifier("presExchange") Exchange exchange){  
    return BindingBuilder  
            .bind(queue)  
            .to(exchange)  
            .with("pres")  
            .noargs();  
}

绑定队列和交换机, 同时设置routingKey "pres" ,noargs( )表示不添加额外的绑定参数


4. 消息持久化

使用 Java Client 时可以这样写:

java 复制代码
@RequestMapping("/pres")  
public String pres() {  
    Message message = new Message("Presistent test...".getBytes(), new MessageProperties());  
    //消息非持久化  
    //message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);  
    //消息持久化  
    message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);  
    System.out.println(message);  
    rabbitTemplate.convertAndSend(Constants.PRES_EXCHANGE, "pres", message);  
    return "消息发送成功";  
}

这里我使用了手动确认的机制,channel.basicNack(deliveryTag, false, false)在消息处理异常时不让他重新回到队列中

java 复制代码
@Component  
public class PresListener {  
  
    @RabbitListener(queues = Constants.PRES_QUEUE)  
    public void handMessage(Message message, Channel channel) throws Exception {  
        long deliveryTag = message.getMessageProperties().getDeliveryTag();  
        try {  
            System.out.printf("接收到消息 : %s , deliverTag : %d%n",  
                    new String(message.getBody(), StandardCharsets.UTF_8),  
                    deliveryTag);  
  
            System.out.println("业务逻辑处理");  
            System.out.println("业务处理完成");  
  
            channel.basicAck(deliveryTag, false);  
        } catch (Exception e) {  
            System.out.println("消息处理失败 :" + e.getMessage());  
            channel.basicNack(deliveryTag, false, false);  
        }  
    }  
}

5. 测试,验证持久化是否生效

先查看消息的发送,消费功能是否正常:

随后我把@RabbitListener这个注解给注释掉,让消息在队列中存储不被消费,随后重启服务, 调用接口发送消息,看队列和消息是否还在

随后我把docker中的RabbitMQ服务重启

java 复制代码
docker restart rabbitmq

这表明我们的消息持久化已经成功实现了


五、补充发送方确认代码示例

1. 开启 Confirm 模式

java 复制代码
spring:
  rabbitmq:
    addresses: amqp://userName:password@RabbitMQ ip地址:5672/vhost
    listener:
      simple:
        acknowledge-mode: manual
    publisher-confirm-type: correlated

publisher-confirm-type: correlated 的作用是开启发送方确认,并允许通过 CorrelationData 关联消息。


2. 配置 ConfirmCallback

java 复制代码
@Bean("confirmRabbitTemplate")
public RabbitTemplate confirmRabbitTemplate(ConnectionFactory connectionFactory) {
    RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);

    rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
        if (ack) {
            System.out.printf("消息发送到交换机成功, id:%s%n", correlationData.getId());
        } else {
            System.out.printf("消息发送到交换机失败, id:%s, cause:%s%n",
                    correlationData.getId(), cause);
        }
    });

    return rabbitTemplate;
}

发送消息:

java 复制代码
@RequestMapping("/confirm")  
public String confirm(){  
    // 1. 创建关联数据对象,ID为"1"  
    CorrelationData correlationData = new CorrelationData("1");  
    // 2. 使用带确认回调的 RabbitTemplate 发送消息  
    confirmRabbitTemplate.convertAndSend(Constants.CONFIRM_EXCHANGE, "confirm", "hello confirm", correlationData);  
    return "消息发送成功";  
}

如果交换机不存在,ack 会是 falsecause 中会提示类似 no exchange

测试交换机不存在

接下来先把exchange修改, 测试一下ConfirmCallback

可以看到交换机不存在直接报错了


3. 配置 ReturnCallback

Confirm 只能判断消息有没有到交换机。

如果交换机存在,但路由键写错了,消息仍然可能进不了队列。

这时需要 Return 模式:

java 复制代码
@Bean("confirmRabbitTemplate")
public RabbitTemplate confirmRabbitTemplate(CachingConnectionFactory connectionFactory) {
    RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);

    rabbitTemplate.setMandatory(true);

    rabbitTemplate.setReturnsCallback(returned -> {
        System.out.println("消息被退回:");
        System.out.println("replyCode = " + returned.getReplyCode());
        System.out.println("replyText = " + returned.getReplyText());
        System.out.println("exchange = " + returned.getExchange());
        System.out.println("routingKey = " + returned.getRoutingKey());
    });

    return rabbitTemplate;
}
测试路由失败:

交换机确认成功,但是没有队列绑定 confirm111,就会触发 ReturnCallback。


六、插入必要的 Mermaid 图示

下面这张图我用来梳理 RabbitMQ 持久化到底要配哪几层。

我一开始只记住了"队列要 durable",但后来发现,真正想降低 RabbitMQ 重启带来的消息丢失风险,需要把交换机、队列、消息三部分放在一起看。
#mermaid-svg-UdKm4ZTy1tbMJWBn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UdKm4ZTy1tbMJWBn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UdKm4ZTy1tbMJWBn .error-icon{fill:#552222;}#mermaid-svg-UdKm4ZTy1tbMJWBn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UdKm4ZTy1tbMJWBn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UdKm4ZTy1tbMJWBn .marker.cross{stroke:#333333;}#mermaid-svg-UdKm4ZTy1tbMJWBn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UdKm4ZTy1tbMJWBn p{margin:0;}#mermaid-svg-UdKm4ZTy1tbMJWBn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UdKm4ZTy1tbMJWBn .cluster-label text{fill:#333;}#mermaid-svg-UdKm4ZTy1tbMJWBn .cluster-label span{color:#333;}#mermaid-svg-UdKm4ZTy1tbMJWBn .cluster-label span p{background-color:transparent;}#mermaid-svg-UdKm4ZTy1tbMJWBn .label text,#mermaid-svg-UdKm4ZTy1tbMJWBn span{fill:#333;color:#333;}#mermaid-svg-UdKm4ZTy1tbMJWBn .node rect,#mermaid-svg-UdKm4ZTy1tbMJWBn .node circle,#mermaid-svg-UdKm4ZTy1tbMJWBn .node ellipse,#mermaid-svg-UdKm4ZTy1tbMJWBn .node polygon,#mermaid-svg-UdKm4ZTy1tbMJWBn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UdKm4ZTy1tbMJWBn .rough-node .label text,#mermaid-svg-UdKm4ZTy1tbMJWBn .node .label text,#mermaid-svg-UdKm4ZTy1tbMJWBn .image-shape .label,#mermaid-svg-UdKm4ZTy1tbMJWBn .icon-shape .label{text-anchor:middle;}#mermaid-svg-UdKm4ZTy1tbMJWBn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UdKm4ZTy1tbMJWBn .rough-node .label,#mermaid-svg-UdKm4ZTy1tbMJWBn .node .label,#mermaid-svg-UdKm4ZTy1tbMJWBn .image-shape .label,#mermaid-svg-UdKm4ZTy1tbMJWBn .icon-shape .label{text-align:center;}#mermaid-svg-UdKm4ZTy1tbMJWBn .node.clickable{cursor:pointer;}#mermaid-svg-UdKm4ZTy1tbMJWBn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UdKm4ZTy1tbMJWBn .arrowheadPath{fill:#333333;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UdKm4ZTy1tbMJWBn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UdKm4ZTy1tbMJWBn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UdKm4ZTy1tbMJWBn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UdKm4ZTy1tbMJWBn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UdKm4ZTy1tbMJWBn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UdKm4ZTy1tbMJWBn .cluster text{fill:#333;}#mermaid-svg-UdKm4ZTy1tbMJWBn .cluster span{color:#333;}#mermaid-svg-UdKm4ZTy1tbMJWBn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UdKm4ZTy1tbMJWBn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UdKm4ZTy1tbMJWBn rect.text{fill:none;stroke-width:0;}#mermaid-svg-UdKm4ZTy1tbMJWBn .icon-shape,#mermaid-svg-UdKm4ZTy1tbMJWBn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UdKm4ZTy1tbMJWBn .icon-shape p,#mermaid-svg-UdKm4ZTy1tbMJWBn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UdKm4ZTy1tbMJWBn .icon-shape .label rect,#mermaid-svg-UdKm4ZTy1tbMJWBn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UdKm4ZTy1tbMJWBn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UdKm4ZTy1tbMJWBn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UdKm4ZTy1tbMJWBn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RabbitMQ 持久化配置
交换机持久化
队列持久化
消息持久化
声明交换机时设置 durable true
RabbitMQ 重启后交换机仍然存在
声明队列时使用 QueueBuilder.durable
RabbitMQ 重启后队列仍然存在
发送消息时设置 PERSISTENT
消息会尽量写入磁盘保存
注意事项
只持久化交换机不够
只持久化队列不够
只持久化消息也不够
持久化会降低一部分吞吐量

这张图里最关键的是中间三条线:

  1. 交换机持久化

    解决的是 RabbitMQ 重启后,交换机元数据是否还存在的问题。

  2. 队列持久化

    解决的是 RabbitMQ 重启后,队列本身是否还存在的问题。

  3. 消息持久化

    解决的是消息进入队列后,是否尽量保存到磁盘的问题。

这里容易忽略的是:持久化不是只配一个地方,而是交换机、队列、消息要结合使用

比如只写了队列持久化:

复制代码
QueueBuilder.durable("confirm_queue").build();

这只能保证队列重启后还在,并不代表消息本身一定还在。

更完整的写法应该像这样:

复制代码
@Bean
public Exchange confirmExchange() {
    return ExchangeBuilder
            .topicExchange("confirm_exchange")
            .durable(true)
            .build();
}

@Bean
public Queue confirmQueue() {
    return QueueBuilder
            .durable("confirm_queue")
            .build();
}

发送关键消息时,再把消息本身设置为持久化:

复制代码
MessageProperties properties = new MessageProperties();
properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);

Message message = new Message("important message".getBytes(), properties);

rabbitTemplate.convertAndSend("confirm_exchange", "confirm", message);

我自己的理解是:

交换机和队列持久化,保证"容器"重启后还在;消息持久化,保证"内容"尽量不丢。

七、说明验证方式

我一般按下面几个场景验证:

  1. 正常发送

    观察 ConfirmCallback 是否 ack=true

  2. 交换机写错

    confirm_exchange 改成 confirm_exchange1,确认是否进入 ack=false 分支。

  3. 路由键写错

    confirm 改成 confirm11,确认是否触发 ReturnCallback。

  4. RabbitMQ 重启

    发送持久化消息后重启 RabbitMQ,查看队列和消息是否还在。

  5. 管理页面观察

    重点看队列中的 ReadyUnacked 状态。


八、总结容易踩坑的地方

  1. durable(true) 只代表交换机或队列持久化,不代表消息一定持久化。
  2. 消息持久化必须配合队列持久化,否则队列没了,消息也没地方恢复。
  3. ConfirmCallback 只确认消息是否到达 Exchange。
  4. ReturnCallback 才能发现消息是否无法路由到 Queue。
  5. mandatory 默认是 false,不设置的话,路由失败的消息可能不会回调给生产者。
  6. 所有消息都持久化会影响性能,关键业务才更适合强可靠配置。
  7. 持久化也不是 100% 绝对可靠,极端情况下消息还没落盘 RabbitMQ 就宕机,仍可能丢失。

九、几套可以直接复用的模板

1. 可靠投递配置模板

yml 复制代码
spring:
  rabbitmq:
    addresses: amqp://user:password@host:5672/vhost
    listener:
      simple:
        acknowledge-mode: manual
    publisher-confirm-type: correlated

2. 持久化队列和交换机模板

java 复制代码
@Bean
public Exchange exchange() {
    return ExchangeBuilder.topicExchange("biz_exchange").durable(true).build();
}

@Bean
public Queue queue() {
    return QueueBuilder.durable("biz_queue").build();
}

@Bean
public Binding binding(Exchange exchange, Queue queue) {
    return BindingBuilder.bind(queue).to(exchange).with("biz.key").noargs();
}

3. 消息持久化模板

java 复制代码
MessageProperties properties = new MessageProperties();
properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);

Message message = new Message("business message".getBytes(), properties);

rabbitTemplate.convertAndSend("biz_exchange", "biz.key", message);

4. 排查提示词模板

复制代码
我现在 RabbitMQ 消息没有被消费者收到。
请按以下链路帮我排查:
1. 生产者是否成功发送到 Exchange
2. ConfirmCallback 是否 ack=true
3. routingKey 是否匹配队列绑定
4. ReturnCallback 是否被触发
5. Queue 和 Message 是否都开启持久化
6. 消费者是否开启手动 ack

结尾总结

  1. 我学习这部分最大的收获是:消息可靠性不是一个配置解决的,而是一条链路一起保证。
  2. 持久化解决的是 RabbitMQ 服务异常后数据尽量不丢的问题。
  3. ConfirmCallback 解决的是消息有没有到达交换机的问题。
  4. ReturnCallback 解决的是消息有没有成功路由到队列的问题。
  5. 只设置队列持久化不够,消息本身也要设置为持久化。
  6. 关键业务更推荐开启持久化、Confirm、Return、消费者手动 Ack。
  7. 可靠性和吞吐量需要权衡,不是所有消息都必须走最高可靠级别。