【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是最后一道保险。以后建队列别手点,一律代码化、可审计。】
相关推荐
lcreek3 小时前
Java 反序列化漏洞深度解析(一):从URLDNS到真正的DNS探测
java·反序列化漏洞
杰克尼3 小时前
天机学堂复习总结(day03-day04)
java·开发语言·redis·elasticsearch·spring cloud
x***r1514 小时前
jdk-11.0.16.1_windows使用步骤详解(附JDK 11环境变量配置与验证教程)
java·开发语言·windows
弹简特4 小时前
【Java项目-轻聊】01-项目演示+项目介绍+准备工作+项目源码
java
luck_bor5 小时前
File类&递归作业
java·开发语言
武子康5 小时前
Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现
java·后端
REDcker7 小时前
Linux OverlayFS详解
java·linux·运维
Royzst7 小时前
xml知识点
java·服务器·前端
鱼鳞_8 小时前
苍穹外卖-Day08(缓存套餐)
java·redis·缓存
过期动态8 小时前
【LeetCode 热题 100】移动零
java·数据结构·算法·leetcode·职场和发展·rabbitmq