跳转至

06 消息队列与投递语义

难度:中级|前置:网络、并发、数据库|目标:理解解耦、确认、重复和积压

MQ 解决什么

消息队列常见价值:

  • 解耦生产者和消费者;
  • 把长任务异步化;
  • 削平短时间流量峰值;
  • 让多个消费者独立处理同一事件;
  • 为失败重试提供边界。

它不自动解决:

  • 业务幂等;
  • 无限积压;
  • 所有顺序问题;
  • 跨数据库和消息的原子性;
  • 消费者代码错误。

一条消息怎样才算成功

需要分别考虑两段:

生产者 → Broker → 消费者
  • Publisher Confirm:Broker 表示已接管消息;
  • Consumer Acknowledgement:消费者表示已处理完成。

二者互相独立。Broker 收到不等于消费者完成;消费者处理完但确认丢失,消息可能重新投递。

投递语义

At most once

最多一次。可能丢,但不主动重投。

At least once

至少一次。尽量不丢,但可能重复。大多数业务系统应该按这种现实设计幂等消费者。

Exactly once

业务端到端的“恰好一次”非常昂贵,通常只能在明确边界和约束内实现。不要只看中间件宣传就认为整个业务不会重复。

消费步骤

可靠顺序通常是:

收到消息
→ 校验
→ 幂等检查
→ 执行业务并保存
→ 确认消息

如果先确认再保存,进程崩溃会丢业务;如果保存成功后确认丢失,会重复投递,因此仍需幂等。

重试和死信

失败要分类:

错误 处理
网络抖动 退避重试
下游限流 延迟重试、降低并发
参数永久错误 进入死信/失败队列
代码 Bug 停止盲目重试并告警
重复消息 返回已完成结果

无限立即重试会形成“重试风暴”。

积压

观察:

  • 入队速率;
  • 消费速率;
  • 未确认数量;
  • 最老消息年龄;
  • 重试数量;
  • 单条处理时间;
  • 消费错误比例。

最老消息年龄往往比单纯队列长度更能反映用户实际延迟。

顺序

全局顺序会限制扩展。更常见的是只保证同一业务键的局部顺序,例如同一 device_id 的命令进入同一分区。

自测

  1. Publisher Confirm 和 Consumer Ack 分别证明什么?
  2. 为什么先 Ack 再写数据库可能丢数据?
  3. 为什么至少一次投递要求消费者幂等?
  4. 哪些错误不应该自动重试?

完成标准

  • 能画出消息从生产到消费的确认边界
  • 能解释三种投递语义
  • 能设计重试和死信分类
  • 能列出五个积压指标

延伸阅读:RabbitMQ 可靠性指南

下一单元:幂等