【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是最后一道保险。以后建队列别手点,一律代码化、可审计。】
相关推荐
咩咩啃树皮6 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚7 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白8 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司9 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地10 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415910 小时前
TCP超时重传机制是为了解决什么问题?
java
莫逸风13 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
z1234567898613 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112314 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python
做个文艺程序员14 小时前
Linux第24篇:Java应用监控体系搭建:Prometheus+Grafana可视化运维
java·grafana·prometheus