消息队列:异步执行链与最终一致性¶
消息队列(MQ)接住的是“已经不该继续阻塞当前请求,但必须保证完成”的后续动作。
1. 异步边界:从同步到事件驱动¶
以“文档上传解析”为例。同步链路要求在一次 HTTP 请求内完成 保存记录 -> OCR 解析 -> 向量化 -> 写索引,这在生产环境下是不可接受的:任何一环抖动都会导致请求超时。
- 数据库定状态,MQ 推进度:请求进入系统,首先在数据库记录
TaskStatus: Pending。写入成功后,系统即宣布“上传成功”。剩余的重活儿通过 MQ 发送到后端 Worker。 - 副作用优先原则:严禁先提交 Offset 再执行业务逻辑。必须遵循:执行副作用 -> 数据库更新状态 -> 提交 Offset。否则一旦 Worker 宕机,系统就会出现“消息已消费但数据未落库”的空洞。
2. Kafka:基于日志的流式存储¶
不要把 Kafka 看成简单的队列,它本质上是分布式分区日志系统。
- Partition 与局部顺序:Kafka 的顺序保证只在 Partition 内部成立。为了保证同一文档的解析顺序,必须使用
document_id作为 Partition Key。 - Consumer Group 与并行度:Partition 是并行度的单位。如果 Topic 只有 2 个分区,你开 10 个 Consumer 也只有 2 个在干活。
- Rebalance(重平衡):这是偶发重复消费的元凶。当 Consumer 加入、退出或心跳超时,Partition 会被重新分配。此时未提交 Offset 的消息会在新节点被重复拉取。解药:业务层必须实现幂等。
3. 消费治理:幂等、重试与死信¶
- 幂等(Idempotency)是入场券:异步链路默认“至少投递一次(At-least-once)”。重复消费是常态,必须通过数据库主键冲突或状态机检查(e.g.,
Update where status='Pending')来封堵重复执行。 - 重试预算:
- 临时故障(网络抖动、三方 429):进入重试队列,采用指数退避(Exponential Backoff)。
- 永久故障(参数错误、权限不足):严禁重试,直接入死信队列(DLQ),等待人工介入。
- Lag(积压)是核心指标:积压意味着消费速度 < 生产速度。第一反应应是增加 Partition 数并同步扩容 Consumer。
4. 选型对比:寻找受力点¶
| 系统 | 核心优势 | 适用场景 |
|---|---|---|
| Kafka | 高吞吐、日志回放、生态成熟 | 海量事件流、日志采集、RAG 异步建库 |
| RocketMQ | 事务消息、延时消息、业务治理强 | 复杂业务逻辑、金融级可靠性 |
| RabbitMQ | 细粒度路由、低延迟 | 任务分发、轻量级异步 |
5. 排查顺序:从进度到资源¶
- 查 Offset 与 Lag:进度卡在哪里了?是某个 Partition 积压,还是全局跟不上?
- 查副作用状态:数据库里的
TaskStatus和 MQ 进度是否匹配?是否存在大量重复执行? - 查 Rebalance 频率:是否因为 Consumer GC 或网络抖动频繁触发重平衡?
- 查存储与 IO:Broker 磁盘是否写满?PageCache 命中率是否下降导致读取变慢?
结论: Offset 只是进度,不是业务真相。真正健壮的异步系统,是靠数据库的状态机收口,靠 MQ 的分区日志推行进度,并始终为“重复消费”留好后路。