跳转至

消息队列:异步执行链与最终一致性

消息队列(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. 排查顺序:从进度到资源

  1. 查 Offset 与 Lag:进度卡在哪里了?是某个 Partition 积压,还是全局跟不上?
  2. 查副作用状态:数据库里的 TaskStatus 和 MQ 进度是否匹配?是否存在大量重复执行?
  3. 查 Rebalance 频率:是否因为 Consumer GC 或网络抖动频繁触发重平衡?
  4. 查存储与 IO:Broker 磁盘是否写满?PageCache 命中率是否下降导致读取变慢?

结论Offset 只是进度,不是业务真相。真正健壮的异步系统,是靠数据库的状态机收口,靠 MQ 的分区日志推行进度,并始终为“重复消费”留好后路。