02-消息队列

什么是消息队列

消息队列可以先粗暴理解成一句话:它是 API 和 Worker 之间的缓冲带与传送带。

在长链路任务里,消息队列并不是“高级可选项”,而是最常见、最实用的基础设施之一。

为什么任务系统几乎总会引入队列

假设一个 AI 识别接口突然在 1 分钟内收到 5000 个任务。

如果没有队列,API 服务通常只剩下两种糟糕选择:

  1. 自己同步做,直接把请求线程打满
  2. 直接调 Worker 服务,瞬间把 Worker 打爆

而引入队列之后,系统会变成这样:

flowchart LR
	A[突发 5000 个请求] --> B[API 快速接收]
	B --> C[消息队列缓冲]
	C --> D1[Worker 1]
	C --> D2[Worker 2]
	C --> D3[Worker 3]
	D1 --> E[状态存储]
	D2 --> E
	D3 --> E

这张图表达的就是消息队列最核心的三个作用:

  • 削峰:高峰来了先排队,不要把执行器压垮
  • 解耦:提交任务和执行任务不是同一个节奏
  • 并行:多个 Worker 可以并发消费

消息队列到底存的是什么

初学者容易误会:队列里是不是要塞完整业务数据?

一般不建议。

更常见的做法是:

  • 队列里只放轻量消息
  • 大对象放数据库或对象存储
  • Worker 消费消息后,再根据 taskId 去取完整上下文

比如一条消息可以非常简单:

{
  "taskId": "task_20260414_001",
  "taskType": "contract_parse",
  "attempt": 1
}

这样设计的原因是:

  • 队列更轻,吞吐更稳
  • 消息重投更简单
  • 数据变更有统一来源,不容易出现消息和数据库不一致

一个最小案例:批量合同解析

假设用户一次性上传了 1000 个合同 PDF。

没有队列时

API 可能会在一次请求里直接做这些事:

  1. 保存文件
  2. 调 OCR 服务
  3. 做字段提取
  4. 写数据库

结果通常是:

  • 请求时间很长
  • OCR 服务被瞬间打满
  • 失败后前端根本不知道做到哪一步

有队列时

流程会变成:

  1. API 为每个文件创建一个任务
  2. taskId 发送到队列
  3. Worker 按自己的消费能力慢慢拿任务
  4. 状态统一写回状态存储

于是系统的压力被平滑了,提交和执行的节奏也分开了。

消息队列的关键语义

消息队列不是简单的“列表”,工程上至少要关注下面四件事。

1. 投递

任务进入队列,等待消费者处理。

2. 消费

Worker 从队列中拉取或订阅消息。

3. 确认

任务处理成功后,要告诉队列“这条消息可以删了”。

4. 重投

如果 Worker 崩了、超时了、处理失败了,消息要能重新投递。

可以用一个简单时序图理解:

sequenceDiagram
	autonumber
	participant API as API
	participant MQ as 消息队列
	participant W as Worker

	API->>MQ: publish(task_id)
	W->>MQ: consume(task_id)
	alt 处理成功
		W->>MQ: ack
	else 处理失败 / 崩溃
		MQ-->>W: timeout / nack
		MQ->>MQ: requeue
	end

这也是为什么很多队列天然和“失败重试”绑在一起讲。

为什么不是 API 直接调用 Worker

理论上可以直连,但问题会很多:

方案 问题
API 直接调 Worker 耦合太紧,Worker 挂了 API 也受影响
API 写 DB,Worker 轮询 DB 轮询效率低,数据库压力大
API 发队列消息 解耦、缓冲、扩展性最好

消息队列最重要的价值,不是“高级”,而是它把不同角色的职责分清楚了。

队列不是状态库,也不是结果库

这是另一个很常见的误区。

队列负责什么

  • 把任务送到消费者手里
  • 提供消费与重投机制
  • 帮系统做流量缓冲

队列不负责什么

  • 不负责持久保存任务完整状态
  • 不负责给前端展示进度
  • 不负责长期保存结果文件

所以真正的工程实践通常是:

  • 队列 管流转
  • 数据库 / Redis 管状态
  • 对象存储 / 数据库 管结果

常见选型怎么理解

技术 更适合什么场景
Redis List / Stream 轻量任务队列、实现简单、延迟低
RabbitMQ 经典业务队列,确认与路由能力强
Kafka 高吞吐事件流、可回放、适合大规模日志与数据流

如果只是做业务任务系统,很多团队一开始用 Redis 或 RabbitMQ 就够了。Kafka 并不是默认更好,而是更偏大规模事件流平台。

初学者最容易踩的坑

1. 把大文件内容直接塞进消息体

结果是队列负担很重,消息重投也变得昂贵。

2. 认为“进了队列就一定只会消费一次”

现实里更常见的是 at-least-once,也就是“至少一次”。

这意味着:

  • 同一任务可能被执行多次
  • 业务层必须做幂等

3. 只上队列,不做状态存储

这样用户只能“提交成功”,却永远不知道任务跑到哪里了。

这一篇要记住的核心点

  1. 消息队列的作用是削峰、解耦、异步和并行
  2. 队列里最好只放轻量消息,完整上下文放状态存储里
  3. 队列解决的是“怎么把任务送去执行”,不是“怎么展示任务状态”

下一篇继续往下看:当队列有了之后,整套任务执行架构应该怎么搭起来。

github