# 留个 message，别等

关于 queue 和事件驱动系统的速成课。21 个概念——Synchronous Call、Message Queue、Asynchronous、Producer 与 Consumer、Broker、Competing Consumers、Command 与 Event、Pub/Sub、Event-driven、Load Leveling、Idempotency、Eventual Consistency、Dead-Letter Queue 等——每个都配有英文名称、一幅画面、一张图和一张带例子的卡片。最后是关系图和一页速查表。核心思想:在时间上解耦工作，正是系统保持快速并在故障中存活的方式。

- **Author:** Fedor Chishchin
- **Level:** intermediate
- **Reading time:** ~35 分钟阅读
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/zh/learn/queues-express

---

## 1. 直接调用的问题

要明白 queue 为什么存在，先得切身体会一下那个简单替代方案的痛:一个服务调用另一个服务并等待它。它一直管用，直到真正要紧的那一刻。

### 地图:六组共 21 个概念

> 一张地铁图:先看清整条线路，再一站一站地坐过去。

先讲**问题**——为什么直接调用会让你失望。然后每组一章:**queue**（想法本身）、**角色**（谁写、谁读、谁保管）、**command 与 event**（两种消息）、**好处**（你付出代价换来了什么）和**难题**（什么会出问题）。最后一章讲什么时候该用 queue、什么时候不该用，另附关系图和速查表。

_Diagram: 六组概念——第 1–6 章依次讲它们。第 7 章讲如何选择，外加速查表。_

### 如何阅读每个概念

> 一本护照:每个人的栏目都一样，所以很容易比较。

从这里开始，每个概念都是同一个结构:它的**英文名称**（记住它——这是你会在 broker 文档和面试里遇到的词）、一幅日常生活的画面、一张**图**，以及一张**卡片**:_问题_、_思路_、_例子_、_代价_，以及它与其他概念的*关联*。最后是关系图和速查表。

### Synchronous Call——一次直接调用把两个服务绑在一起

> 给某人打电话，然后一直占着线直到他们把事情做完——你不能挂断，不能做别的任何事，而如果对方不接，你就被卡住了。

服务 A 让服务 B 做点事，最显而易见的办法是 **synchronous call**（同步调用）:A 发问，然后等待，一直扣着手上的一切，直到 B 回答。这把它们紧紧地**耦合**在一起——A 的速度被 B 的速度绑架，B 没做完 A 就没法继续。如果只是快速答复，这没问题。但对缓慢或繁重的工作，A 的时间都花在了原地冻结、等别人上。

_Diagram: A 调用了 B 然后原地不动:B 的所有慢活儿，都是 A 被阻塞的时间。_

#### Synchronous Call (request–response（请求–响应）)

- **问题:** 调用方被冻结，直到被调用方回答。
- **思路:** 发问、等答复、再继续。最简单的交谈方式。
- **例子:** 订单服务通过 HTTP 调用价格服务，等拿到价格后才显示购物车。
- **代价:** 紧密的**耦合**:A 的速度和可用性取决于 B。
- **关联:** 它的痛点由 **Message Queue** 来治；需要即时答复时，它仍是最佳选择。

### Failure Propagation——被调用方挂了，调用方也跟着坏

> 打电话给仓库确认订单，因为没人接，你就拒绝接收任何新订单——一扇关上的门，堵死了整家店。

更深的问题是故障。如果 A 调用时 B 挂了或过载了，A 的请求也会失败——故障沿着调用链一路**向上传播**。一个缓慢或损坏的服务就能让所有依赖它的东西停摆，流向 B 的流量激增也会把 A 一起拖垮。紧密耦合意味着弱点会蔓延，系统的可用性只取决于它最脆弱的那一环。

_Diagram: 仓库挂了——错误一路向上爬:订单失败，结账失败。_

#### Failure Propagation (cascading failure（级联故障）)

- **问题:** 一个服务出故障，会变成所有调用方都出故障。
- **思路:** 同步调用链的可用性，只取决于最弱的那一环。
- **例子:** 仓库不响应 → 订单报错 → 结账不能用了。
- **代价:** 流向一个服务的流量激增，会拖垮它上面的一切。
- **关联:** 用缓冲来治:**Message Queue** 带来 **Resilience**——故障变成延迟。

### Background Work——有些工作不该让用户等

> 在收银台，你付了钱，拿着小票就走——不会站在柜台前看他们打包、发货、给你发邮件。慢的部分在你走后才进行。

很多工作不需要在用户收到响应之前完成——发确认邮件、生成发票、缩放照片。这就是 **background work**（后台工作）。让用户等这些全部做完，既慢又没必要。你想要的是接下请求、快速答复，再让慢的部分随后进行。直接调用做不到这一点；它让所有人为所有事情等待。

_Diagram: API 立刻答复；邮件、发票和照片稍后处理——从 queue 里取出来做。_

#### Background Work (background jobs（后台任务）)

- **问题:** 用户在等一些其实与他们的响应无关的工作。
- **思路:** 接下请求，快速答复，慢的部分之后再做。
- **例子:** 付款成功——马上给小票；邮件、发票和照片缩放放到后台。
- **代价:** 后台工作的结果不会立刻出现。
- **关联:** 用 **Message Queue** 和 **Asynchronous Processing** 来实现。

> **核心问题:** “调用方真的需要等这件事做完吗——还是只是占着线，等一项与它无关的慢活儿跑完？”

> 直接调用把两个服务在时间上耦合起来:调用方要等，还要承受被调用方的缓慢和故障。对于缓慢或不紧急的工作，这就是个陷阱。

## 2. queue

解决办法是别再调用，改为发消息。queue 是中间的缓冲，让一边留下工作、另一边来取——在时间上把两者解耦。

### Message Queue——丢下消息，继续往前走

> 留一条语音留言，而不是在电话那头干等:说清你要什么，挂断，继续过你的一天——他们有空时自会处理。

A 不再调用 B 并等待，而是写一条描述工作的 **message**（消息），把它丢进 **queue**——一条让消息排队等待处理的队伍。A 立刻就自由了，可以继续；它不在乎 B 什么时候处理。这条消息是一个小包裹，写着“需要做的事”，是被交接出去，而不是当场执行。这次交接就是全部的想法。

_Diagram: A 丢下一条消息就继续走；消息在 queue 里等待；B 准备好后取走一条。下方——一条消息本身可能长什么样。_

#### Message Queue (queue（队列）)

- **问题:** 直接调用让发送方一直占着线。
- **思路:** 把工作描述成一条 **message** 放进 **queue**；接收方自己来取。
- **例子:** 不去调用邮件服务——而是在 queue 里放一条带订单号的 `SendEmail` 消息。
- **代价:** 多了一个需要运行和理解的活动部件。
- **关联:** 带来 **Temporal Decoupling**；由 **Broker** 保管；由 **Consumer** 读取。

### Temporal Decoupling——queue 是时间上的缓冲

> 办公桌上的收件盘:工作堆在里面，被平稳地处理，所以突然涌来的一堆文件不会压垮那个人——只是让盘子暂时变高了一点。

queue 夹在两边之间，作为**缓冲**，在消息被处理之前先保管着。这就**在时间上把它们解耦**（temporal decoupling）:发送方和接收方不再需要同时保持快速、可用，甚至不需要同时在运行。突发产生的工作可以被平稳地消费。queue 吸收了工作到来的速度与能完成的速度之间的差距。

_Diagram: 左边工作成批涌来，右边被平稳处理。中间的 queue 吸收了两者的差距。_

#### Temporal Decoupling (buffer in time（时间上的缓冲）)

- **问题:** 双方必须在同一时刻都活着、都够快。
- **思路:** 在它们之间放一个缓冲:发送方和接收方各按各的节奏工作。
- **例子:** 一阵突发的订单堆在 queue 里，以稳定的速度被处理。
- **代价:** 工作可能要在 queue 里等——结果不是即时的。
- **关联:** **Resilience** 和 **Load Leveling** 的基础。

### Asynchronous Processing——不等结果

> 寄一封信，而不是当面交谈——寄出后就继续忙自己的，相信对方之后会读到并处理，不用你站在旁边。

这就是**异步**工作:发送方不等结果。它把消息发出去就继续，结果在之后、在带外发生。你放弃了直接调用那种即时答复，换来的是不再被阻塞。对于任何不需要立刻答复的事，这笔交易是快速、有韧性的系统的基础。

_Diagram: 上:同步，A 站着等。下:异步，A 发出消息继续干活，由 worker 稍后完成任务。_

#### Asynchronous Processing (async（异步）)

- **问题:** 在等一个你此刻并不需要的结果。
- **思路:** 发出消息，继续往前；结果在之后、在带外发生。
- **例子:** 报告在后台生成；准备好后用户再拿到。
- **代价:** 没有即时答复；更难追踪发生了什么。
- **关联:** **Synchronous Call** 的反面；会带来 **Eventual Consistency**。

> **queue 之问:** “我能不能把这件事变成丢进 queue 的一条消息——让发送方立刻继续，工作等对方准备好再做？”

> queue 是发送方与接收方之间的缓冲。丢下一条消息就继续走——工作在时间上被解耦，等对方准备好时再异步完成。

## 3. producer、consumer、broker

三种角色让 queue 运转起来，给它们起好名字，就能理清大部分行话。一边制造消息，一边处理消息，中间有个东西把消息稳妥地保管起来。

### Producer / Consumer——一边制造工作，一边完成工作

> 一间厨房:服务员把点菜单钉在架子上，厨师一张张取下来做。两边从不直接说话——那排单子架就是全部接口。

创建消息的一方是 **producer**（或 publisher）；处理消息的一方是 **consumer**（或 worker）。producer 丢下一张单子；consumer 取走一张，做完活，再取下一张。它们互相不认识也不互相等待——只认识 queue。这种干净的分离，让每一边都能各自构建、扩展和修改。

_Diagram: producer 往 queue 里写，consumer 从 queue 里读。它们从不直接碰面。_

#### Producer / Consumer (publisher / worker)

- **是什么:** **producer** 创建消息；**consumer** 取走消息并完成工作。
- **思路:** 双方只认识 queue，不认识彼此。
- **例子:** 网页应用丢下“缩放这张照片”；worker 取走消息并完成缩放。
- **陷阱:** producer 不知道工作何时、由谁来完成。
- **关联:** **Broker** 位于两者之间；一个 queue 上有很多 consumer，就是 **Competing Consumers**。

### Message Broker——broker 稳妥地保管消息

> 寄件人和收件人之间的邮局:它收下你的信，妥善保管，直到收件人来取——所以即使对方不在，也不会丢。

中间是 **message broker**——像 **RabbitMQ** 或 **Apache Kafka** 这样的软件，负责接收消息、可靠地存储，并把它们交给 consumer。它就是系统的邮局。可靠性是关键:即使所有 consumer 都挂了，broker 也会把消息保管好，直到有人准备处理，所以工作是被搁置，而不是丢失。

_Diagram: broker 接收并可靠地存储消息。consumer 挂了——消息在等待，而不是消失。_

#### Message Broker (broker（消息代理）)

- **问题:** 消息需要一个安全的地方存放，直到有人取走。
- **思路:** 专门的软件接收、存储并投递消息——系统的邮局。
- **例子:** **RabbitMQ**、**Apache Kafka**。
- **代价:** 又多一个需要运行和维护的系统。
- **关联:** 保管 **Message Queue**；**Resilience** 建立在它的可靠性之上。

### Competing Consumers——加 consumer 就能更快

> 点菜单堆起来时，你就往流水线上多加几个厨师——每人取下一张单子，积压清得更快，而订单进来的方式不用改。

因为 consumer 只是取下一条消息，所以你可以让**很多 consumer 并行**地消费同一个 queue——这就是 **Competing Consumers** 模式。积压在增长？多加几个 worker，它们会自动分担负载——每个抓取不同的消息。这是 queue 的一大优势:你可以通过增加 consumer 独立地扩展慢的那一部分，而不用碰 producer 或 queue 本身。

_Diagram: 一个 queue 上有三个 worker:各取各的消息。再加一个——积压消得更快。_

#### Competing Consumers (worker pool（工作池）)

- **问题:** 一个 worker 跟不上；积压越来越多。
- **思路:** 多个 consumer 读同一个 queue；每条消息只交给其中一个。
- **例子:** 大促之前——照片缩放 worker 从两个加到十个。
- **代价:** 并行处理会打乱顺序——见 **Message Ordering**。
- **关联:** **Independent Scaling** 的基础；适用于 **Command** 消息。

> **角色之问:** “这些消息由谁生产、谁消费，我能不能只靠给同一个 queue 多加几个 consumer 就清掉积压？”

> producer 创建消息，consumer 处理消息，broker 在中间稳妥保管。需要更高吞吐？加 consumer——它们自动分担这个 queue。

## 4. event 与 command

一条消息可能是两种截然不同的东西:一道要求做某事的命令，或一则某事已经发生的通告。第二种会悄悄改变你设计整个系统的方式。

### Command——command 告诉一个 worker 去做一项任务

> 交给某个特定部门的一张工单:“缩放这张图片。”它指明任务，期望恰好一个团队去做，然后就结束了。

**command** 消息是指向一个 consumer 的直接指令:“发这封邮件”“处理这笔付款”。这是把 queue 当作待办清单来用——producer 知道该发生什么，把任务交出去，由取走它的那个来做一次。这是 queue 最简单的用法:把具体的工作卸出去，稍后恰好做一次。

_Diagram: SendEmail 命令:“做这件事。”恰好一个 consumer 取走它。_

#### Command (command message（命令消息）)

- **是什么:** 一条命令式的消息:“做这个”。用动词命名。
- **如何运作:** producer 知道该发生什么；由一个 consumer 完成任务。
- **例子:** `SendEmail`、`ProcessPayment`、`ResizeImage`。
- **陷阱:** producer 仍然知道需要什么工作——还留有一些耦合。
- **关联:** **Event** 的反面；常由 **Competing Consumers** 来处理。

### Event——event 宣告某件事已经发生

> 布告栏上的全公司通告:“刚刚有一位新客户注册了。”它不告诉任何人该做什么——谁关心，谁就按自己的方式反应。

**event**（事件）不一样:它陈述一个关于过去的事实——“订单已下”“用户已注册”——而不说谁该做什么。producer 只是宣告它，不知道也不关心谁在听。也许没人反应；也许有五个服务反应。这颠倒了关系:你不再命令某个特定的 worker，而是广播某件事成立了，让感兴趣的各方自己决定。

_Diagram: OrderPlaced 事件:“这件事发生了。”producer 不知道谁在听。_

#### Event (event message（事件消息）)

- **是什么:** 关于过去的事实:“这件事发生了”。用过去时命名。
- **如何运作:** producer 宣告它，不知道谁在听:零个、一个或五个服务。
- **例子:** `OrderPlaced`、`UserRegistered`。
- **陷阱:** 更难看清最终会发生什么——反应分散在各个服务里。
- **关联:** **Command** 的反面；通过 **Pub/Sub** 投递；**Event-driven** 的基础。

### Publish/Subscribe——一个 event，多种反应

> 一份报纸只印一次，送到每位订阅者手里，每个人出于各自的原因阅读——体育迷、投资人、填字游戏爱好者——都来自同一期。

这种广播模型就是 **publish/subscribe**（pub/sub，发布/订阅）:producer 把 event **发布**到一个 **topic**，每个感兴趣的 consumer **订阅**后拿到自己的一份副本。一个“订单已下”的 event 可以同时触发邮件服务、分析服务和发货服务——这就是 **fan-out**（扇出）。发布者甚至不知道它们的存在。以后可以随时加新的反应，完全不用动发布者。

_Diagram: 一个 event 进入 topic——每个订阅者都拿到自己的副本:这就是 fan-out。_

#### Publish/Subscribe (pub/sub, topic, fan-out)

- **问题:** 一个 event 对很多服务都重要，而且名单还在增长。
- **思路:** 发布到一个 **topic**；每个订阅者拿到**自己的副本**（**fan-out**）。
- **例子:** “订单已下”→ 邮件、分析、发货同时做出反应。
- **代价:** 每份副本各自单独处理；整条反应链很难看清。
- **关联:** 投递 **Event**；**Event-driven Architecture** 建立在它之上。

### Event-driven Architecture——event 解开了“谁认识谁”的耦合

> 给报纸增加一位新订阅者，丝毫不会改变报纸的写法——发布者根本不需要知道谁在读。

这就是 **event-driven**（事件驱动）设计的深层力量:event 的 producer 与所有对它做出反应的一方完全解耦。想加一个响应“订单已下”的新功能，只需订阅一个新的 consumer——订单服务不用改。这样构建的系统靠增加监听者来成长，而不是靠修改被监听的对象。大型系统就是这样演进的，而不至于所有东西互相依赖。

_Diagram: 新功能就是新订阅者。订单服务不变。_

#### Event-driven Architecture (EDA)

- **问题:** 每个新功能都意味着要去修改“什么都在那里发生”的那个服务。
- **思路:** 服务宣告 event；功能作为新的监听者加入。
- **例子:** 按订单发放积分——让一个新服务订阅“订单已下”。
- **代价:** **Eventual Consistency**，而且在消息之间跳转的请求更难调试。
- **关联:** 建立在 **Event** 和 **Pub/Sub** 之上；带来 **Independent Scaling**。

> **pub/sub 之问:** “我是在命令某个特定的 worker 去做一项任务——还是在宣告某件事发生了，让任意数量的服务自己做出反应？”

> command 告诉一个 consumer 去做一项任务。event 宣告一个事实，让任何人都能反应——而 pub/sub 把一个 event 扇出给许多方，解开了“谁认识谁”的耦合。

## 5. 为什么它强大

queue 增加了活动部件，所以它必须物有所值。它确实做到了，因为它换来了三样别处很难得到的东西:韧性、平稳应对流量高峰，以及独立扩展。

### Resilience——consumer 挂了，只是工作被延迟

> 如果厨房关门一小时，点菜单就在架子上等着——重新开门后，厨师把积压的单子逐一做完。没有一张订单丢失。

因为 broker 保管着消息，consumer 崩溃不会丢工作——消息会等到它回来，然后被处理。对比直接调用，服务挂了就是彻底失败。queue 把“服务挂了”从硬错误变成了暂时的延迟。这种**韧性**（resilience）——工作经受住故障并恢复——往往是团队选用 queue 的最大单一理由。

_Diagram: 上:B 挂了——请求失败。下:B 挂了——工作在 queue 里等待，B 恢复后继续。_

#### Resilience (fault tolerance（容错）)

- **问题:** 用直接调用时，服务挂了意味着所有人都报错。
- **思路:** broker 保管消息；故障变成延迟。
- **例子:** 邮件 worker 崩溃一小时——邮件在 queue 里等着，重启后发出。
- **代价:** consumer 挂着的时候，工作会堆积并延迟完成。
- **关联:** 治愈 **Failure Propagation**；依赖 **Broker** 的可靠性。

### Load Leveling——吸收流量高峰

> 山洪与小镇之间的一座水库:洪峰涌入水库，水库再以稳定、安全的速度向下游放水。

当一大波请求同时涌来——一场大促、一次病毒式传播——queue 让你可以**削峰填谷**（level the load）:高峰填满 queue，consumer 按自己稳定的节奏把它排空。下游缓慢的系统从来看不到高峰，只看到可控、均匀的流量。处理积压所带来的压力叫做 **backpressure**（背压）。没有 queue，同样的高峰会直接冲击系统，很可能把它冲垮。

_Diagram: 请求高峰填满 queue，consumer 平稳地把它排空。下游系统看不到高峰。_

#### Queue-Based Load Leveling (load leveling, backpressure（削峰、背压）)

- **问题:** 流量高峰直接打到缓慢的系统上，把它冲垮。
- **思路:** 高峰填满 queue；consumer 按自己的节奏处理。
- **例子:** 一场大促:每分钟成千上万的订单，被当作平稳的流量来处理。
- **代价:** 积压增长，工作延迟完成（**backpressure**）——要盯着 queue 的长度。
- **关联:** 建立在 **Temporal Decoupling** 之上；**Competing Consumers** 负责清掉积压。

### Independent Scaling——每一边各自扩展、各自演进

> 你可以多雇厨师、换厨房设备、再加一个工位——这一切都不用改变服务员点单的方式，因为他们只接触那排单子架。

解耦意味着每一边都可以**独立地**变化（independent scaling）。按需求增减 consumer；用另一种语言重写某个 consumer；新增一个对现有 event 做出反应的全新 consumer——全都不用碰 producer。queue 是系统各部分之间一道稳定的接缝，而稳定的接缝正是让大系统能一块一块地成长和变化、而不是一次性全部改动的原因。

_Diagram: queue 是一道稳定的接缝。在右边你可以扩展、重写、新增——左边什么都不用变。_

#### Independent Scaling (independent scaling and evolution（独立扩展与演进）)

- **问题:** 改一个部分，就得动所有部分。
- **思路:** queue 是稳定的接缝；每一边各自变化。
- **例子:** 一个 worker 被改用另一种语言重写——producer 根本没察觉。
- **代价:** 消息格式变成了一份契约——修改要谨慎。
- **关联:** 由 **Competing Consumers** 和 **Event-driven Architecture** 实现。

> **好处之问:** “我用 queue 是为了获得韧性、吸收高峰，还是独立扩展——一个足以证明多出一个活动部件值得的具体好处？”

> queue 换来韧性（工作经得起崩溃）、削峰（高峰被吸收）和独立扩展——这些都是直接调用根本给不了你的好处。

## 6. 新出现的难题

异步消息解决了真实的问题，也制造了新的问题。没有一个是致命的，但假装它们不存在，正是基于 queue 的系统出现隐蔽、令人抓狂的 bug 的原因。

### At-least-once 与 Idempotency——消息可能到达两次

> 为保险起见，邮局有时会再投递一份它不确定是否送到的信的副本——宁可两次也别没有，但现在你可能会对同一条指示执行两次。

大多数 broker 保证 **at-least-once**（至少一次）投递:它们宁可把消息发两次，也不冒丢失的风险，所以 consumer 偶尔会收到同一条消息不止一次。如果“向客户扣款”执行了两次，那就是真问题。解决办法是 **idempotency**（幂等）:把工作设计成做两次与做一次效果相同（先检查“是否已经扣过款？”）。假定会有重复；让重复变得无害。

_Diagram: broker 把“给订单 42 扣款”发了两次。consumer 先检查“是否已扣款？”——于是直接跳过第二条。_

#### Idempotency (at-least-once delivery（至少一次投递）)

- **问题:** **At-least-once** 投递:同一条消息可能到达不止一次。
- **思路:** 把工作设计成执行两次与执行一次效果相同。
- **例子:** 扣款前先检查“订单 42 是否已扣款？”——如果是，就跳过。
- **代价:** 你需要在某个地方记住哪些事已经做过。
- **关联:** 每个 **Consumer** 都必须具备；与 **Dead-Letter Queue** 一起，让重试变得安全。

### Message Ordering——顺序没有保证

> 按先后顺序寄出的两封信，到达时可能是任意顺序——所以如果第二步比第一步先到，收信人就会一头雾水。

消息很多、consumer 又并行时，消息不一定按发送的顺序被处理。如果“更新地址”和“删除账号”乱序到达，结果就是一团乱。有些系统能以一定代价在某个类别内保持顺序；通常更省事的办法是把工作设计成不依赖严格顺序。不管怎样，除非你专门做了安排，否则**绝不要假定有顺序**。

_Diagram: 先发了“更新地址”，再发了“删除账号”。结果被反过来处理——于是你在更新一个已经不存在的账号。_

#### Message Ordering (ordering guarantees（顺序保证）)

- **问题:** 并行的 consumer 不会按发送的顺序处理消息。
- **思路:** 不要依赖顺序——或在某个类别内显式地安排顺序。
- **例子:** “删除账号”先于“更新地址”被处理——结果一团乱。
- **代价:** 严格的顺序需要花功夫，还会妨碍并行。
- **关联:** **Competing Consumers** 的代价；**Eventual Consistency** 的近邻。

### Eventual Consistency——结果最终会一致

> 一则通知在办公室里传开:有那么一会儿，有些人知道、有些人不知道，直到消息传到每个人，大家又一致了。

因为反应是异步发生的，系统是**最终一致**的（eventually consistent）:event 刚发生后，不同部分可能短暂地不一致——订单已存在，但分析系统还没把它统计进去。它很快就会追上，但“到处都瞬间一致”这件事没有了。你要围绕这个间隙来设计，给用户展示合理的中间状态，而不是假定所有部分在同一瞬间更新。

_Diagram: 订单已经存在，但分析系统还没统计它。一个短暂的间隙——然后大家又一致了。_

#### Eventual Consistency (consistent in the end（最终一致）)

- **问题:** event 刚发生后，系统的各个部分会短暂地看到不同的东西。
- **思路:** 接受这个间隙:每个部分都会追上并达成一致——只是晚一点。
- **例子:** 订单已创建，但报表里还没有；片刻之后就有了。
- **代价:** 你得为用户设计中间状态（“处理中……”）。
- **关联:** **Asynchronous Processing** 和 **Event-driven Architecture** 的结果。

### Dead-Letter Queue——有毒的消息需要一个去处

> 一封没人能处理的信——字迹模糊、根本没法办——不能无休止地重试、堵住整条队伍。它会被放到一个特别的托盘里，等人来查看。

有些消息永远无法被成功处理——格式错误，或者对应的数据已经不存在。如果放任不管，这样的 **poison message**（毒消息）会被无休止地重试，并堵住 queue。标准的解决办法是 **dead-letter queue**（死信队列）:失败几次之后，把消息挪到一个单独的 queue 里等待检查，这样它就不再毒害整个流程。要为会失败的消息做好打算，否则一条坏消息就会让它后面的所有消息都停滞。

_Diagram: 正常消息顺利通过。有毒的那条，尝试三次之后，被移入 dead-letter queue——等人来检查。_

#### Dead-Letter Queue (DLQ)

- **问题:** **poison message** 每次都失败，被无休止地重试，并堵住 queue。
- **思路:** 失败几次之后，把消息挪到一个单独的 queue 里等待检查。
- **例子:** 一条关于已删除用户的消息，尝试三次后进入 DLQ。
- **代价:** 得有人盯着 DLQ，否则它会变成丢失工作的坟场。
- **关联:** 只有配合 **Idempotency**，重试才安全；让流程持续运转，从而保障 **Resilience**。

> **难题之问:** “我有没有让这项工作可以安全地执行两次、不依赖任何顺序假设、能容忍短暂的不一致——而且消息处理不了的时候，它们去哪儿？”

> 异步消息带来重复（用 idempotency 应对）、没有保证的顺序、最终一致，以及会失败的消息（用 dead-letter queue）。这四样都要提前规划。

## 7. 用好它

queue 是一件强力工具，不是默认选项。真正的功夫在于:解耦确实划算时才去用它，简单更值钱时就保留直接调用。

### Queue vs Direct Call——等待本身是问题时，就用 queue

> 你领一个号，离开去等一项你稍后再来取的慢服务——但如果只是快速问一句是或不是，你就直接在柜台问，因为领号比等一下还要可笑。

当工作**缓慢、有高峰或不紧急**，当你想让调用方保持快速，或者当好几个服务都应该对同一件事做出反应时，就用 queue。当你需要立刻得到答复才能继续时——查价格、查权限——就保留**直接调用**，因为在那里，简单和即时的结果比解耦更值钱。选工具，要看等待是否真的是问题。

_Diagram: 一个问题:你需要答案才能继续吗？需要——直接调用。不需要——queue。_

#### Queue vs Direct Call (how to choose（如何选择）)

- **问题:** 调用方需要答案才能继续吗？
- **Queue:** 工作缓慢、有高峰、不紧急，或者很多方都需要（fan-out）。
- **直接调用:** 你需要即时答复:查价格、查权限。
- **Queue 的代价:** broker、重复、顺序、eventual consistency、更难追踪。
- **关联:** 在 **Synchronous Call** 与 **Message Queue** 之间做选择。

### 别条件反射般地用它

> 为了给隔壁桌的人递张字条，装上一整套邮件收发和分拣系统——这套机器的成本，已经超过了它要解决的问题。

queue 会带来实实在在的复杂度:要运行一个 broker，要处理重复和顺序，要围绕 eventual consistency 做设计，而且追踪一个现在要在消息之间跳转的请求也更困难。对于一个简单的系统、两部分之间只是一次快速调用，这些开销不值得。当某个具体的好处——韧性、规模、解耦——足以证明代价合理时，再加 queue，而不是因为事件驱动听起来很高级。

### 这些概念是相互关联的

> 一条传送带:从一个痛点到一剂解药——而这剂解药有它自己的好处，也有它自己的副作用。

整体画面如下。**synchronous call** 的痛点，由 **message queue** 来治，并由 **broker** 保管。**command**（给一个 consumer）和 **event**（给很多个，经由 **pub/sub**）从 queue 中流过。这换来了**好处**——韧性、削峰、扩展——也带来了**难题**:重复、顺序、eventual consistency 和 DLQ。

_Diagram: 本课概念之间的主要联系:从直接调用，到 queue、它的好处和它的代价。_

#### 添加 queue 之前

- **等待是不是问题**——这项工作是否足够缓慢、有高峰或不紧急，值得解耦？
- **command 还是 event**——是一个 worker 做一项任务，还是许多服务对一个事实做出反应？
- **工作是否幂等**——既然消息可能到达两次，执行两次是否安全？
- **它是否假定了顺序**，我是安排好了顺序，还是在设计上消除了对顺序的依赖？
- **系统能否容忍**短暂的最终不一致？
- **失败的消息去哪儿**——有没有 dead-letter queue？

#### 你用好 queue 的迹象

- 你把**缓慢、有高峰、不紧急**的工作放进 queue，把**直接调用**留给即时答复。
- 你的 consumer 是**幂等**的，所以重复消息不会造成伤害。
- 除非明确安排过，否则你**不假定有顺序**。
- 你为 **eventual consistency** 做设计，而不是期待瞬间一致。
- 失败的消息落进 **dead-letter queue**，而不是堵住队伍。

## 速查表:全部 21 个概念

### 直接调用的问题

- **Synchronous Call** (request–response): 调用并等待。简单，但在时间上把服务耦合在一起。
- **Failure Propagation** (cascading failure): 一个服务出故障，会拖垮所有调用它的服务。
- **Background Work** (background jobs): 用户不该等的工作:邮件、发票、照片。

### queue

- **Message Queue** (queue): 丢下一条消息就继续走；接收方自己来取。
- **Temporal Decoupling** (buffer in time): 一个缓冲:双方不必在同一时刻运行。
- **Asynchronous Processing** (async): 不等结果；结果在之后发生。

### 角色

- **Producer / Consumer** (publisher / worker): 一边写消息，一边处理消息；它们只认识 queue。
- **Message Broker** (broker): 可靠地存储和投递消息的软件:RabbitMQ、Kafka。
- **Competing Consumers** (worker pool): 一个 queue 上的多个 worker 分担负载。

### command 与 event

- **Command** (做这件事): “做这件事”——恰好一个 consumer 去做。
- **Event** (这件事发生了): “这件事发生了”——任何人都可以反应，也可以没人反应。
- **Publish/Subscribe** (pub/sub, topic, fan-out): 一个 event 进入 topic——每个订阅者都拿到自己的副本。
- **Event-driven Architecture** (EDA): 功能作为新的监听者加入，而不是去修改发布者。

### 好处

- **Resilience** (fault tolerance): consumer 崩溃只是延迟，不是丢失工作。
- **Queue-Based Load Leveling** (load leveling, backpressure): 高峰填满 queue；consumer 平稳地把它排空。
- **Independent Scaling** (independent evolution): 每一边各自扩展、各自变化。

### 难题

- **Idempotency** (at-least-once): 重复会发生——让它们变得无害。
- **Message Ordering** (ordering guarantees): 除非你做安排，否则顺序没有保证。
- **Eventual Consistency** (consistent in the end): 系统的各部分短暂地不一致，然后追上。
- **Dead-Letter Queue** (DLQ): 一个专门存放反复失败的消息的 queue。

### 选择

- **Queue vs Direct Call** (如何选择): 需要答案才能继续——直接调用；不需要——queue。

> queue 是深思熟虑的选择:当在时间上解耦能换来韧性、规模或 fan-out 时再用它——而当即时答复比这一切都重要时，就保留直接调用。

速成课结束 · 7 章 · 21 个概念

接下来是实践:挑一件你的应用目前在请求里同步做的慢事——发邮件、生成报告——把它挪到 queue 后面，让用户得到即时响应，由 worker 在之后做慢的那部分。然后让这个 worker 可以安全地执行两次。当你亲眼看到响应变快而工作照样完成时，这个模式就豁然开朗了。但请把一个想法放在最前面:转变在于从调用并等待，变成留个消息然后继续。在时间上解耦工作，你的系统会变得更快、更结实、更容易成长——代价是要用事件而不是命令来思考。
