RocketMQ 怎么保证消息不丢失?
结论:RocketMQ 的不丢要靠四段一起守:生产端同步发送并检查
SendStatus、Broker 端同步刷盘(Sync Flush)与同步双写(Sync Replication)、消费端处理成功才返回CONSUME_SUCCESS、以及重试与死信队列兜底。任何一段用默认的异步配置,都会在恰好那一段出问题时丢消息。
一、消息的四段生命周期
Producer ──① send 超时/异常被吞──> Broker(主) ──② 只写 PageCache 未落盘──> Disk
│
└─③ 未复制到从节点,主磁盘损坏 ──> 从
Consumer ──④ 先返回成功再异步处理,处理失败 → 位点前移,消息丢失
| 环节 | 典型丢失原因 | 对策 |
|---|---|---|
| 生产端 | 发送异常只打日志、sendOneWay、重试次数为 0 | 同步 send + 检查状态 + 重试 + 本地补偿表 |
| Broker 存储 | 异步刷盘时机器掉电 | flushDiskType=SYNC_FLUSH 或依赖多副本 |
| 主从复制 | 异步复制时主节点磁盘损坏 | brokerRole=SYNC_MASTER(同步双写) |
| 消费端 | 先返回成功再异步处理、异常被 catch 后仍返回成功 | 处理成功才返回 CONSUME_SUCCESS,失败返回重试 |
| 重试耗尽 | 失败消息被丢弃 | 监控 %DLQ% 死信队列并人工/自动补偿 |
二、生产端要做的三件事
DefaultMQProducer producer = new DefaultMQProducer("order_producer");
producer.setNamesrvAddr("10.0.0.11:9876");
producer.setRetryTimesWhenSendFailed(2); // 同步发送失败重试
producer.setSendMsgTimeout(3000);
producer.start();
try {
SendResult r = producer.send(msg); // 同步发送,不用 sendOneWay
if (r.getSendStatus() != SendStatus.SEND_OK) {
outboxMapper.insert(msg); // 落本地补偿表,定时任务重投
}
} catch (Exception e) {
outboxMapper.insert(msg); // 网络异常同样落表
}
要点:sendOneWay 没有返回值,等于放弃确认;SendStatus 除了 SEND_OK 还有 FLUSH_DISK_TIMEOUT、FLUSH_SLAVE_TIMEOUT、SLAVE_NOT_AVAILABLE,后三者表示"消息写进去了但没达到可靠性要求",业务上应当按失败处理或补偿。
三、Broker 端配置
# broker-a.properties:同步刷盘 + 同步双写
brokerRole=SYNC_MASTER
flushDiskType=SYNC_FLUSH
# 从节点 broker-a-s.properties
brokerRole=SLAVE
flushDiskType=SYNC_FLUSH
| 配置 | 异步(默认) | 同步 |
|---|---|---|
flushDiskType | ASYNC_FLUSH:后台每 500ms 刷一次,掉电丢最多 500ms 数据 | SYNC_FLUSH:每条消息 fsync 后才返回,用组提交降低开销 |
brokerRole | ASYNC_MASTER:从节点追主节点的进度 | SYNC_MASTER:等从节点写入成功才返回 |
同步刷盘并不是"每条都单独 fsync":Broker 会把等待中的请求合并成一批一起刷(组提交),所以 QPS 高时单条成本会被摊薄,但低峰期 RT 明显上升。
四、消费端要做的两件事
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, ctx) -> {
try {
for (MessageExt m : msgs) {
orderService.handle(m); // 幂等处理(唯一键去重)
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; // 全部成功才成功
} catch (Exception e) {
log.error("consume failed", e);
return ConsumeConcurrentlyStatus.RECONSUME_LATER; // 触发重试
}
});
- 不要"返回成功 + 丢线程池异步处理":进程重启时线程池里的任务直接消失;
- 重试默认 16 次,间隔逐级拉长,最终进入
%DLQ%消费组名,必须监控这个 Topic 的堆积。
五、常见追问
- 同步刷盘会不会扛不住? 高频写入时组提交能撑住十万级;如果业务是低峰期小流量,RT 会从毫秒升到十几毫秒,要按业务分级配置。
- 只开同步双写不开同步刷盘,会丢吗? 两台机器同时掉电才会丢;单机故障不丢。这是性价比最高的组合。
- 从节点挂了同步双写怎么办? 主节点等不到从节点确认,发送会返回
FLUSH_SLAVE_TIMEOUT,延迟升高甚至阻塞;需要监控 HA 连接状态,必要时临时切换为异步并告警。 - 消费重试 16 次后消息去哪了? 进入死信队列
%DLQ%groupName,默认只保留 3 天,必须有告警和补偿脚本,否则就是静默丢消息。 - 消息过期删除算不算丢? 算。
fileReservedTime默认 72 小时,未消费的消息会随 CommitLog 一起被清理,堆积场景下尤其危险。
小结
- 不丢是四段配置的乘积:同步发送、同步刷盘、同步双写、成功才确认。
- 生产端识别
SendStatus的四种状态,非SEND_OK一律走补偿。 - 消费端最怕"假成功",处理逻辑必须同步完成或至少落本地任务表。
- 死信队列与消息过期是两个静默丢消息的出口,必须配监控。