RocketMQ 怎么保证消息不丢失:端到端详细做法(生产者→Broker→消费者)
结论先讲清楚:RocketMQ 天然提供的是 “至少一次(At-least-once)” 投递语义:不丢 的代价通常是 可能重复。
所以“消息不丢失”= 端到端链路都做到可确认、可恢复、可重试,并在业务侧做 幂等/去重。
0. 先把“丢消息”拆开看:到底可能丢在哪?
一条消息从发送到被业务真正“生效”,大概经过 3 段:
- 生产者发送阶段:消息还没可靠到达 Broker(网络抖动、超时、客户端崩溃)
- Broker 存储阶段:消息到了 Broker 但没落盘/没复制完成(异步刷盘、异步复制、机器掉电)
- 消费者处理阶段:消息被拉到了消费者,但业务没处理成功或 offset 提交不当(消费成功标记丢、异常没重试)
要做到“不丢”,每段都要有对应的“确认点(ack)”和“恢复手段”。
1. 生产者侧:保证“发出去一定可追踪、可重试、可补偿”
1.1 禁用 OneWay(单向)发送
sendOneway没有任何发送结果,只适合日志/监控这种“丢了也无所谓”的场景。- 需要可靠性:用 同步 send 或 异步 send + 回调确认。
1.2 同步发送:以 sendResult 为准,失败就重试/落库补偿
同步发送示例(最常用、最稳):
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();
关键点:
- 必须检查
SendResult和SendStatus,别只 try-catch。 - 失败不能只打日志:至少要进入重试/补偿(见 1.4)。
1.3 异步发送:回调必须处理失败分支
异步发送的坑:很多人只写成功回调,失败回调里“打印一下就算了”——这就是丢消息。
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// OK
}
@Override
public void onException(Throwable e) {
// 必须做补偿:重试/落库/报警
}
});
1.4 本地消息表/Outbox:最硬的“不丢”方案(强烈推荐)
如果你要做到“服务宕机/重启也不丢”,只靠 client 重试不够。推荐经典 Outbox(本地消息表):
流程:
- 在业务 DB 事务里:写业务数据 + 写一条“待发送消息”(消息表状态=NEW)
- 事务提交后:后台任务扫描 NEW 状态消息,发送到 MQ
- 发送成功:更新消息表状态=SUCCESS;失败:记录失败次数,继续重试 + 告警
- 下游消费:用 msgKey/业务单号做幂等
优点:
- 生产者宕机也不会丢(因为消息在 DB 里)
- 能做可视化、补发、审计
示意表结构:
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 侧的核心是两件事:
- 刷盘策略(落盘)
- 主从复制策略(高可用)
2.1 刷盘:ASYNC_FLUSH vs SYNC_FLUSH
- ASYNC_FLUSH(异步刷盘):性能高,但机器突然掉电可能丢“尚未刷到磁盘”的数据
- SYNC_FLUSH(同步刷盘):Broker 收到消息后,刷盘完成才返回成功给生产者,可靠性更高,但吞吐更低
在追求“不丢”的场景(支付、订单状态等),建议优先 SYNC_FLUSH。
broker.conf 示例:
# 同步刷盘:更可靠(推荐关键链路)
flushDiskType=SYNC_FLUSH
2.2 主从复制:ASYNC_MASTER vs SYNC_MASTER
- 异步复制:Master 返回成功时,Slave 可能还没复制到,Master 宕机会丢
- 同步复制(SYNC_MASTER):Master 必须等 Slave 复制完成才返回成功(更可靠,性能降低)
broker.conf 示例:
# 同步复制:更可靠(推荐关键链路)
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):
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 监控 + 补偿工具/脚本
1470

被折叠的 条评论
为什么被折叠?



