06 消息队列与投递语义¶
难度:中级|前置:网络、并发、数据库|目标:理解解耦、确认、重复和积压
MQ 解决什么¶
消息队列常见价值:
- 解耦生产者和消费者;
- 把长任务异步化;
- 削平短时间流量峰值;
- 让多个消费者独立处理同一事件;
- 为失败重试提供边界。
它不自动解决:
- 业务幂等;
- 无限积压;
- 所有顺序问题;
- 跨数据库和消息的原子性;
- 消费者代码错误。
一条消息怎样才算成功¶
需要分别考虑两段:
- Publisher Confirm:Broker 表示已接管消息;
- Consumer Acknowledgement:消费者表示已处理完成。
二者互相独立。Broker 收到不等于消费者完成;消费者处理完但确认丢失,消息可能重新投递。
投递语义¶
At most once¶
最多一次。可能丢,但不主动重投。
At least once¶
至少一次。尽量不丢,但可能重复。大多数业务系统应该按这种现实设计幂等消费者。
Exactly once¶
业务端到端的“恰好一次”非常昂贵,通常只能在明确边界和约束内实现。不要只看中间件宣传就认为整个业务不会重复。
消费步骤¶
可靠顺序通常是:
如果先确认再保存,进程崩溃会丢业务;如果保存成功后确认丢失,会重复投递,因此仍需幂等。
重试和死信¶
失败要分类:
| 错误 | 处理 |
|---|---|
| 网络抖动 | 退避重试 |
| 下游限流 | 延迟重试、降低并发 |
| 参数永久错误 | 进入死信/失败队列 |
| 代码 Bug | 停止盲目重试并告警 |
| 重复消息 | 返回已完成结果 |
无限立即重试会形成“重试风暴”。
积压¶
观察:
- 入队速率;
- 消费速率;
- 未确认数量;
- 最老消息年龄;
- 重试数量;
- 单条处理时间;
- 消费错误比例。
最老消息年龄往往比单纯队列长度更能反映用户实际延迟。
顺序¶
全局顺序会限制扩展。更常见的是只保证同一业务键的局部顺序,例如同一 device_id 的命令进入同一分区。
自测¶
- Publisher Confirm 和 Consumer Ack 分别证明什么?
- 为什么先 Ack 再写数据库可能丢数据?
- 为什么至少一次投递要求消费者幂等?
- 哪些错误不应该自动重试?
完成标准¶
- 能画出消息从生产到消费的确认边界
- 能解释三种投递语义
- 能设计重试和死信分类
- 能列出五个积压指标
延伸阅读:RabbitMQ 可靠性指南
下一单元:幂等