RocketMQ怎么保证消息不丢失详解

RocketMQ 怎么保证消息不丢失:端到端详细做法(生产者→Broker→消费者)

结论先讲清楚:RocketMQ 天然提供的是 "至少一次(At-least-once)" 投递语义:不丢 的代价通常是 可能重复

所以"消息不丢失"= 端到端链路都做到可确认、可恢复、可重试 ,并在业务侧做 幂等/去重


0. 先把"丢消息"拆开看:到底可能丢在哪?

一条消息从发送到被业务真正"生效",大概经过 3 段:

  1. 生产者发送阶段:消息还没可靠到达 Broker(网络抖动、超时、客户端崩溃)
  2. Broker 存储阶段:消息到了 Broker 但没落盘/没复制完成(异步刷盘、异步复制、机器掉电)
  3. 消费者处理阶段:消息被拉到了消费者,但业务没处理成功或 offset 提交不当(消费成功标记丢、异常没重试)

要做到"不丢",每段都要有对应的"确认点(ack)"和"恢复手段"。


1. 生产者侧:保证"发出去一定可追踪、可重试、可补偿"

1.1 禁用 OneWay(单向)发送

  • sendOneway 没有任何发送结果,只适合日志/监控这种"丢了也无所谓"的场景。
  • 需要可靠性:用 同步 send异步 send + 回调确认

1.2 同步发送:以 sendResult 为准,失败就重试/落库补偿

同步发送示例(最常用、最稳):

java 复制代码
DefaultMQProducer producer = new DefaultMQProducer("pg_order");
producer.setNamesrvAddr("127.0.0.1:9876");

// 发送超时,建议结合网络情况调整
producer.setSendMsgTimeout(3000);

// 同步失败重试次数(注意:重试可能导致重复,需要幂等)
producer.setRetryTimesWhenSendFailed(3);

// 异步发送失败重试次数(若你使用 async)
producer.setRetryTimesWhenSendAsyncFailed(3);

producer.start();

Message msg = new Message(
        "TOPIC_ORDER",
        "CREATE",
        ("orderId=1001").getBytes()
);

// 建议设置业务唯一键:用于追踪/去重
msg.setKeys("ORDER_1001");

SendResult result = producer.send(msg);
if (result.getSendStatus() != SendStatus.SEND_OK) {
    // 这里不要"吞掉",必须进入补偿链路
    throw new RuntimeException("send failed: " + result);
}

producer.shutdown();

关键点:

  • 必须检查 SendResultSendStatus,别只 try-catch。
  • 失败不能只打日志:至少要进入重试/补偿(见 1.4)。

1.3 异步发送:回调必须处理失败分支

异步发送的坑:很多人只写成功回调,失败回调里"打印一下就算了"------这就是丢消息。

java 复制代码
producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        // OK
    }

    @Override
    public void onException(Throwable e) {
        // 必须做补偿:重试/落库/报警
    }
});

1.4 本地消息表/Outbox:最硬的"不丢"方案(强烈推荐)

如果你要做到"服务宕机/重启也不丢",只靠 client 重试不够。推荐经典 Outbox(本地消息表)

流程:

  1. 在业务 DB 事务里:写业务数据 + 写一条"待发送消息"(消息表状态=NEW)
  2. 事务提交后:后台任务扫描 NEW 状态消息,发送到 MQ
  3. 发送成功:更新消息表状态=SUCCESS;失败:记录失败次数,继续重试 + 告警
  4. 下游消费:用 msgKey/业务单号做幂等

优点:

  • 生产者宕机也不会丢(因为消息在 DB 里)
  • 能做可视化、补发、审计

示意表结构:

sql 复制代码
CREATE TABLE t_outbox_msg (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  biz_key VARCHAR(64) NOT NULL,         -- 如 orderId
  topic VARCHAR(64) NOT NULL,
  tag VARCHAR(64),
  body TEXT NOT NULL,
  status TINYINT NOT NULL,              -- 0 NEW / 1 SUCCESS / 2 FAIL
  retry_count INT NOT NULL DEFAULT 0,
  next_retry_time DATETIME,
  create_time DATETIME NOT NULL,
  update_time DATETIME NOT NULL,
  UNIQUE KEY uk_biz_key (biz_key)
);

这套方案本质上:把"是否发出去"变成一件 可恢复的状态机,而不是"希望 MQ 不出错"。

1.5 事务消息:适合"跨系统一致性"

RocketMQ 有事务消息(半消息 + 本地事务 + 回查),适合:

  • 你需要"业务提交了就一定最终发出消息"
  • 且允许"最终一致"

但是事务消息并不是万能银弹:你仍然要处理 回查、幂等、补偿


2. Broker 侧:保证"收到就持久化、并且最好复制到至少 1 台机器"

Broker 侧的核心是两件事:

  1. 刷盘策略(落盘)
  2. 主从复制策略(高可用)

2.1 刷盘:ASYNC_FLUSH vs SYNC_FLUSH

  • ASYNC_FLUSH(异步刷盘):性能高,但机器突然掉电可能丢"尚未刷到磁盘"的数据
  • SYNC_FLUSH(同步刷盘) :Broker 收到消息后,刷盘完成才返回成功给生产者,可靠性更高,但吞吐更低

在追求"不丢"的场景(支付、订单状态等),建议优先 SYNC_FLUSH

broker.conf 示例:

properties 复制代码
# 同步刷盘:更可靠(推荐关键链路)
flushDiskType=SYNC_FLUSH

2.2 主从复制:ASYNC_MASTER vs SYNC_MASTER

  • 异步复制:Master 返回成功时,Slave 可能还没复制到,Master 宕机会丢
  • 同步复制(SYNC_MASTER):Master 必须等 Slave 复制完成才返回成功(更可靠,性能降低)

broker.conf 示例:

properties 复制代码
# 同步复制:更可靠(推荐关键链路)
brokerRole=SYNC_MASTER

实战建议:关键 Topic 用"同步刷盘 + 同步复制",非关键 Topic 用异步提升性能。

2.3 多副本/一致性:DLedger(可选但很强)

如果你需要更强的高可用(类似 Raft 多副本),可以考虑 RocketMQ 的 DLedger (基于 Raft 的 CommitLog 复制)。

优点:

  • 不是传统主从"单 Slave",可以 N 副本
  • Leader 挂了可自动选主

代价:

  • 运维复杂度更高
  • 性能略受影响(但换来更强可靠性)

2.4 磁盘"写满"也是一种"隐形丢消息"

Broker 磁盘使用率到阈值后,可能拒绝写入或进入保护模式。要做:

  • 监控磁盘使用率、CommitLog 目录
  • 合理设置保留策略与告警阈值

3. 消费者侧:保证"处理成功才 ACK,失败能重试,最终能落到 DLQ"

3.1 正确 ACK:处理成功才返回 CONSUME_SUCCESS

Push 模式下(MessageListenerConcurrently):

java 复制代码
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, ctx) -> {
    for (MessageExt msg : msgs) {
        try {
            // 1) 业务处理(落库/调用下游)
            handle(msg);

            // 2) 幂等(强烈建议):比如用 msg.getKeys()/业务单号做去重
        } catch (Exception e) {
            // 返回稍后重试:RocketMQ 会重新投递
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});

关键点:

  • 不要在 catch 里吞异常然后返回 SUCCESS(这等于"业务没成功但告诉 MQ 成功了",就是丢)
  • 批量消息要特别注意:任何一条失败都要返回 RECONSUME_LATER(或只消费一条一条确认)

3.2 配置最大重试次数 + 死信队列(DLQ)

RocketMQ 默认会重试多次,超过阈值会进 DLQ(死信队列)。你要做的是:

  • 设置合理的 maxReconsumeTimes
  • 对 DLQ 做监控、人工/自动补偿

3.3 幂等/去重:不丢的代价是可能重复

常见做法:

  • 使用 msg.getKeys() 或业务主键作为幂等键
  • 消费成功后记录到 DB(唯一索引)或 Redis SETNX
  • 再次收到相同幂等键则直接 ACK(返回 SUCCESS)

DB 幂等示例思路:

  • 建表 t_msg_consume_log(msg_key unique, consume_time, ...)
  • 处理前插入,插入失败说明已处理过 → 直接返回 SUCCESS

3.4 消费与落库一致性:把"业务成功"定义清楚

你要明确:什么叫"消费成功"?

  • 如果业务是写 DB:以 DB 事务提交成功为准
  • 如果是调用外部系统:要么对方幂等,要么你有补偿任务

建议:把"外部调用成功"也落一条本地状态(可恢复),否则你无法证明"没丢"。


4. 端到端最佳实践组合(按可靠性等级给你配方)

4.1 普通业务(大多数场景够用)

  • 生产者:同步 send + 校验 sendResult + 失败重试
  • Broker:异步刷盘 + 异步复制(默认)
  • 消费者:失败返回 RECONSUME_LATER + 幂等

特点:吞吐高,偶发极端故障可能丢极少量(掉电窗口)。

4.2 关键链路(订单、支付、资金类)

  • 生产者:Outbox 本地消息表(或事务消息) + 同步 send
  • Broker:SYNC_FLUSH + SYNC_MASTER(并做 HA/多机房规划)
  • 消费者:严格 ACK + 幂等 + DLQ 补偿闭环
  • 配套:监控告警(发送失败率、积压、DLQ、磁盘、主从延迟)

特点:可靠性强,成本是吞吐下降 + 架构更复杂。


5. 最容易踩坑的"丢消息"清单(对照自查)

  • ✅ 用了 sendOneway
  • ✅ 异步发送没处理 onException(或失败只打印日志)
  • ✅ 同步发送不检查 SendResult/SendStatus
  • ✅ send 失败后没有"落库补偿/重试任务"
  • ✅ Broker 用异步刷盘 + 机器掉电/宕机
  • ✅ Master 异步复制,Master 挂了 Slave 没追上
  • ✅ Consumer catch 后仍返回 SUCCESS
  • ✅ 没幂等,重复消息被当成"异常"处理导致回滚/乱套
  • ✅ DLQ 不监控,死信消息等于"业务上丢了"

6. 一页纸 Checklist(上线前必过)

生产者

  • 禁止 OneWay(除非允许丢)
  • 同步 send 检查 SEND_OK;失败走补偿
  • 关键链路使用 Outbox 或事务消息
  • msgKey/业务唯一键可追踪

Broker

  • 关键 Topic:SYNC_FLUSH
  • 关键 Topic:SYNC_MASTER 或 DLedger 多副本
  • 磁盘、主从延迟、存储异常告警

消费者

  • 成功才 ACK,失败 RECONSUME_LATER
  • 幂等(DB 唯一键/Redis SETNX)
  • DLQ 监控 + 补偿工具/脚本

相关推荐
名誉寒冰2 小时前
深入理解fd_set:从基础到实战应用(Linux/C++)
java·linux·c++
灵魂猎手2 小时前
Antrl4 入门 —— 使用Antrl4实现一个表达式计算器
java·后端
zhonghua8810162 小时前
spring ai alibab agent之ReactAgent深度解读
java·人工智能·spring
水坚石青2 小时前
Java+Swing+Mysql实现物业管理系统
java·开发语言·数据库·mysql·swing
尼古拉斯·纯情暖男·天真·阿玮2 小时前
[JavaEE初阶] Thread类的基本用法
java·开发语言
Wpa.wk2 小时前
自动化测试(java) - PO模式了解
java·开发语言·python·测试工具·自动化·po模式
IT 行者2 小时前
Spring Security 7.0 新特性详解
java·后端·spring
华仔啊2 小时前
Java 的金额计算用 long 还是 BigDecimal?资深程序员这样选
java·后端
Coder_Boy_3 小时前
业务导向型技术日志记录(2)
java·人工智能·驱动开发·微服务