--- tags: [MQ, 消息确认, 持久化, ACK, 可靠性] create time: 2026-05-24 19:52 --- # MQ 消息确认与持久化 ## 概述 消息从 Producer 发出到 Consumer 处理完成,中间经历多个环节,任何一个环节出问题都可能导致消息丢失或重复。本文从三种投递语义出发,分别剖析 Producer 端、Broker 端、Consumer 端的确认与持久化机制,帮助你理解如何在可靠性与性能之间做出合理取舍。 ## 正文 ### 三种投递语义 消息投递语义定义了"一条消息最多/至少/恰好被投递几次"的承诺,是所有可靠性设计的理论基础。 | 语义 | 含义 | 典型场景 | 代价 | |------|------|----------|------| | **At-Most-Once** | 消息最多投递一次,可能丢失 | 日志采集、监控指标上报 | 不丢就行,丢了也无所谓 | | **At-Least-Once** | 消息至少投递一次,可能重复 | 订单创建、支付通知 | 业务端需做幂等处理 | | **Exactly-Once** | 消息恰好投递一次 | 资金转账、库存扣减 | 实现复杂,性能开销大 | > [!question] > 实际工程中,At-Most-Once 和 At-Least-Once 哪个更常用?为什么 Exactly-Once 很少被端到端保证? 严格意义上的 Exactly-Once 在分布式系统中几乎不可能端到端实现。大多数 MQ 选择 At-Least-Once 作为默认语义,把去重的责任交给业务层。这样设计的原因是:网络不可靠、进程可能宕机、任何一跳的 ACK 都可能丢失——与其追求完美的一次投递,不如让每层做好自己的事。 ### Producer 端确认 Producer 发送消息后,需要确认 Broker 是否成功接收。不同确认方式对应不同的可靠性级别。 **同步发送 + ACK**:Producer 发送后阻塞等待 Broker 返回 ACK。最可靠,但吞吐最低。适合对可靠性要求极高的场景,如金融交易。 **异步发送 + 回调**:Producer 发送后立即返回,通过回调函数处理结果。吞吐高,但回调可能乱序,且如果 Producer 在回调前宕机,消息可能丢失。 **重试机制**:网络抖动或 Broker 暂时不可用时,Producer 会自动重试。需要注意:重试可能导致重复消息,所以 Broker 端需要配合去重(如 Kafka 的幂等 Producer)。 ```go // 同步发送:阻塞等待 Broker 确认 msg := &sarama.ProducerMessage{ Topic: "order-events", Value: sarama.StringEncoder(`{"order_id": "1001"}`), } partition, offset, err := producer.SendMessage(msg) if err != nil { // 发送失败,根据策略决定是否重试 log.Printf("send failed: %v", err) } // partition 和 offset 说明 Broker 已确认收到 log.Printf("sent to partition %d, offset %d", partition, offset) ``` 同步发送的优势在于语义简单——`SendMessage` 返回成功就代表 Broker 已经持久化了这条消息。代价是每条消息都要等一次网络往返,吞吐受限于 RTT。 ### Broker 端持久化 Broker 收到消息后,需要将其写入存储。这里有两个关键决策点:**刷盘策略**和**主从同步模式**。 **同步刷盘 vs 异步刷盘**: - **同步刷盘**:消息写入 Page Cache 后立即调用 `fsync` 落盘。可靠性高,但每次写入都要等磁盘 I/O,吞吐受限于磁盘性能。 - **异步刷盘**:消息写入 Page Cache 后立即返回,由后台线程批量刷盘。吞吐极高(利用了操作系统的 Page Cache),但 Broker 突然宕机时,Page Cache 中未刷盘的消息会丢失。 **主从同步模式**: - **同步复制**:Producer 发送的消息必须被所有(或多数)副本确认后才算写入成功。可靠但延迟更高。 - **异步复制**:主节点确认后即返回,副本异步拉取。延迟低,但主节点宕机时可能丢失尚未同步的数据。 > [!question] > Kafka 的 ISR(In-Sync Replicas)机制是如何在同步复制和异步复制之间找到平衡的? Kafka 的 ISR 是一个动态集合,只有与 Leader 保持同步的 Follower 才在 ISR 中。Producer 通过 `acks=all` 配置要求 ISR 中所有副本确认。如果某个 Follower 落后太多,会被踢出 ISR。这样既保证了可靠性(ISR 中的副本都有最新数据),又避免了慢副本拖累整体性能。 ### Consumer 端确认 Consumer 从 Broker 拉取消息后,需要告知 Broker "我已经处理完了",这就是 Consumer 端的 ACK。 **手动 ACK vs 自动 ACK**: - **自动 ACK**:Consumer 拉取消息后立即自动确认。实现简单,但如果 Consumer 在处理消息前宕机,消息就丢了——因为 Broker 以为它已经被消费了。 - **手动 ACK**:Consumer 处理完业务逻辑后显式调用 ACK。更可靠,但需要开发者自己管理确认时机。 **ACK 时机的选择**: - **收到即 ACK**:拉取到消息就确认,然后在本地处理。风险等同于自动 ACK。 - **处理完再 ACK**:业务逻辑执行成功后再确认。如果处理过程中 Consumer 崩溃,消息会被重新投递(At-Least-Once)。这是生产环境的推荐做法。 ### 端到端消息投递流程 下图展示了从 Producer 到 Consumer 的完整投递链路,标注了每个环节的确认方式: ```mermaid sequenceDiagram participant P as "Producer" participant B as "Broker" participant C as "Consumer" P->>B: "发送消息" Note right of P: "同步等待 / 异步回调 / 重试" B->>B: "写入 Page Cache" Note over B: "同步刷盘 / 异步刷盘" B->>B: "主从同步复制" Note over B: "同步复制 / 异步复制" B-->>P: "ACK 确认" C->>B: "拉取消息" B-->>C: "返回消息" C->>C: "处理业务逻辑" Note over C: "手动 ACK / 自动 ACK" C-->>B: "ACK 确认消费完成" B->>B: "更新消费偏移量" ``` 整个链路中,每一跳的 ACK 都可能因为网络或进程故障而丢失。这就是为什么端到端的 Exactly-Once 极难实现——你需要在每个环节都做好容错。 ### Go 代码:手动 ACK 的消费者示例 ```go func consumeWithManualAck(consumer sarama.ConsumerGroup) error { handler := &consumerGroupHandler{} // 消费者组自动管理分区分配和 Rebalance for { if err := consumer.Consume(context.Background(), []string{"order-events"}, handler); err != nil { return err } } } // consumerGroupHandler 实现 sarama.ConsumerGroupHandler 接口 type consumerGroupHandler struct{} func (h *consumerGroupHandler) Setup(_ sarama.ConsumerGroupSession) error { return nil } func (h *consumerGroupHandler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil } func (h *consumerGroupHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { for msg := range claim.Messages() { // 先处理业务逻辑 if err := processOrder(msg.Value); err != nil { log.Printf("process failed: %v, will retry", err) continue // 不 ACK,消息会被重新投递 } // 业务处理成功后再手动确认 sess.MarkMessage(msg, "") } return nil } ``` 这段代码的核心逻辑是:**先消费,再确认**。如果 `processOrder` 返回错误,我们选择不调用 `MarkMessage`,这样消息不会被标记为已消费,下次拉取时会再次投递。这就是 At-Least-Once 的典型实现——宁可重复处理,也不能丢失消息。 ## 关联笔记 - [[05-可靠性保障/13-MQ-Exactly-Once-语义|MQ Exactly-Once 语义]] - [[05-可靠性保障/14-MQ-消息幂等性|MQ 消息幂等性]] - [[05-可靠性保障/15-MQ-顺序性保障|MQ 顺序性保障]] - [[05-可靠性保障/16-MQ-死信队列与消息回溯|MQ 死信队列与消息回溯]] - [[07-主流MQ对比/22-Kafka|Kafka]]