【Broker一重启消息没了:一次RabbitMQ非持久化+没开Confirm的血亏事故】

【今天下午正准备摸鱼,运维突然在群里甩了一句:"RabbitMQ重启了一下,你们订单对账炸了。"我脑瓜子嗡的一下------重启一回,怎么就漏单了?我立刻开抓。**


事故现场

  • 现象:
    • 中午维护把MQ Broker滚动重启了一波;
    • 上游生产方说"发送成功没异常",但下游消费者统计到一段时间内消息缺口;
    • 对账单里一大片空洞,正好卡在Broker重启那几分钟。

日志随手一截:

复制代码
2026-03-20 12:01:32 INFO  o.s.a.r.core.RabbitTemplate - send order.created ok
2026-03-20 12:01:40 WARN  c.xxx.order.OrderConsumer - expected seq=102345, got=102348, missing=[102346,102347]

RabbitMQ管理台:那段时间内队列曲线出现"平地断层",没有重新入队迹象。

当场怀疑三件事:

  1. 队列/交换机是不是非持久化;
  2. 消息是不是没设持久化;
  3. 生产端根本没开publisher confirm,自己觉得发出去了,其实没落盘。

排查链路(一步步对线)

  1. 先看队列参数:

    rabbitmqctl list_queues name durable arguments
    order.created false [] <-- 直接裂开,durable=false

  2. 查交换机:

    rabbitmqctl list_exchanges name durable
    amq.direct true
    order.ex false <-- 交换机也不持久

  3. 抓生产端配置,果然是"经验主义"写法:

yaml 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: none     # 没开Confirm
    publisher-returns: false         # 没开Return
# 队列声明在控制台手点的,默认非持久

发送代码也很"轻快":

java 复制代码
// 反例:没显式持久化,没Confirm/Return回调
rabbitTemplate.convertAndSend("order.ex", "order.created", orderDto);

这仨组合拳一打:Broker重启窗口内,非持久化队列/交换机直接没了,消息也没持久化到磁盘,发送端又没确认回调,事后还坚信"我发了"。离谱但真实。


根因总结

  • 队列/交换机 durable=false → 服务重启/节点重启后配置及未消费消息直接蒸发;
  • 消息未持久化(deliveryMode≠PERSISTENT)→ 即便队列持久化,未持久化消息仍可能丢;
  • 未开启 Publisher Confirm / Return → 生产端对"没落盘/没路由"完全无感。

修复方案(一步到位)

目标:持久化、可观测、可补救。

1) 队列/交换机全部持久化,优先用 Quorum Queue

java 复制代码
@Bean
public Declarables orderBindings() {
    // 推荐Quorum队列(RabbitMQ 3.8+),天然多副本、崩了也不丢
    Map<String, Object> quorumArgs = new HashMap<>();
    quorumArgs.put("x-queue-type", "quorum");

    Exchange ex = ExchangeBuilder.directExchange("order.ex")
            .durable(true)           // 交换机持久
            .build();

    Queue q = QueueBuilder.durable("order.created.q") // 队列持久
            .withArguments(quorumArgs)
            .build();

    Binding b = BindingBuilder.bind(q).to((DirectExchange) ex).with("order.created");
    return new Declarables(ex, q, b);
}

如果还在用经典队列(classic),至少要 durable=true,且有镜像策略/迁移到quorum。

2) 生产端开启 Confirm + Return,并显式持久化

yaml 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true
    template:
      mandatory: true   # 路由不到队列时触发Return
java 复制代码
@Configuration
public class RabbitProducerConfig implements InitializingBean {
    @Autowired private RabbitTemplate rabbitTemplate;

    @Override
    public void afterPropertiesSet() {
        rabbitTemplate.setConfirmCallback((correlation, ack, cause) -> {
            if (!ack) {
                log.error("MQ NACK, id={}, cause={}",
                        correlation != null ? correlation.getId() : null, cause);
                // 入补偿表/队列重试
            }
        });
        rabbitTemplate.setReturnsCallback(returned -> {
            log.error("MQ RETURN: exchange={}, routingKey={}, replyText={}, body={}",
                    returned.getExchange(), returned.getRoutingKey(),
                    returned.getReplyText(), new String(returned.getMessage().getBody(), StandardCharsets.UTF_8));
            // 兜底处理:换路由/落告警
        });
    }
}

public void sendOrder(OrderDTO dto){
    Message msg = MessageBuilder.withBody(JSON.toJSONBytes(dto))
        .setContentType(MessageProperties.CONTENT_TYPE_JSON)
        .setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 持久化消息
        .build();
    rabbitTemplate.convertAndSend("order.ex", "order.created", msg,
            new CorrelationData(dto.getId().toString()));
}

3) 消费端:手动ACK + 幂等

和这次事故直接关系不大,但兜底必须要有。

java 复制代码
@RabbitListener(queues = "order.created.q", ackMode = "MANUAL")
public void onMessage(Message message, Channel channel) throws IOException {
    OrderDTO dto = JSON.parseObject(message.getBody(), OrderDTO.class);
    try {
        // 幂等:按照bizId/订单号唯一键去重
        orderService.createIfAbsent(dto);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        // 不可恢复错误 -> DLQ;可恢复 -> 延迟重试
        channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
    }
}

4) 生产端"最终一致性"兜底:Outbox(强烈安利)

  • 业务落库和出站消息同时写入同一库事务:t_order + t_outbox;
  • 独立消息中转器扫描t_outbox可靠投递,投递成功再删;
  • 就算MQ整体不可用/Confirm失败,也不会丢------最多是"稍后送达"。

验证

  • 切换到Quorum+持久化消息后,模拟Broker重启:队列与消息都在,Confirm回调稳定ACK;
  • 手工制造路由错误,立刻触发Return回调并告警;
  • 压测1小时:无新增缺口,对账恢复正常;
  • Outbox通道打开时,即使临时断开MQ,消息也会在连通后被补投。

踩坑总结

  • durable=false + 非持久化消息 + 没开Confirm = 组合技,重启必挨打;
  • 生产端一定要有Publisher Confirm/Return和补偿通道(Outbox/重试队列);
  • 队列推荐Quorum,消费侧幂等+手动ACK是最后一道保险。以后建队列别手点,一律代码化、可审计。】
相关推荐
daidaidaiyu7 小时前
一文学习 工作流开发 BPMN、 Flowable
java
SuniaWang8 小时前
《Spring AI + 大模型全栈实战》学习手册系列 · 专题六:《Vue3 前端开发实战:打造企业级 RAG 问答界面》
java·前端·人工智能·spring boot·后端·spring·架构
sheji34168 小时前
【开题答辩全过程】以 基于springboot的扶贫系统为例,包含答辩的问题和答案
java·spring boot·后端
m0_726965989 小时前
面面面,面面(1)
java·开发语言
xuhaoyu_cpp_java9 小时前
过滤器与监听器学习
java·经验分享·笔记·学习
程序员小假10 小时前
我们来说一下 b+ 树与 b 树的区别
java·后端
Meepo_haha11 小时前
Spring Boot 条件注解:@ConditionalOnProperty 完全解析
java·spring boot·后端
sheji341611 小时前
【开题答辩全过程】以 基于springboot的房屋租赁系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端