RocketMQ 怎么保证消息不丢失?

#中间件#面试八股

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
配置异步(默认)同步
flushDiskTypeASYNC_FLUSH:后台每 500ms 刷一次,掉电丢最多 500ms 数据SYNC_FLUSH:每条消息 fsync 后才返回,用组提交降低开销
brokerRoleASYNC_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 一律走补偿。
  • 消费端最怕"假成功",处理逻辑必须同步完成或至少落本地任务表。
  • 死信队列与消息过期是两个静默丢消息的出口,必须配监控。
笔记加载中…