# 12. 同步与异步

如何在一次同步请求和一个异步事件之间做选择,以及 queue、outbox、retry 和 backpressure 各自的位置在哪里。

- **Author:** Fedor Chishchin
- **Level:** advanced
- **Reading time:** 约 25 分钟阅读
- **Published:** 2026-05-23
- **Source:** https://fedorthinks.com/zh/learn/software-architect-12-sync-async

---

## 1. 核心观点

选同步还是异步,是在"立刻给出答案"和"在时间维度上可靠地处理"之间做选择。

### 同步调用适合一个问题

> 如果收银员问一件商品的价格,答案现在就要。如果商店给会计送一份报表,那可以走队列。

当用户现在就需要结果时,同步请求很好:校验密码、展示价格、确认权限。它对 UX 和调试都更简单,但它把调用方和被调用方的可用性绑在了一起。

异步适合那些可以稍后完成的活儿:邮件、索引、analytics、webhook 投递、报表生成、enrichment。它降低耦合,但要求 status、retry、dead-letter queue 和 observability。

### event 是一个事实,不是一个命令

> "门开着"是一个事实。"把门打开"是一个命令。你可以把两者混在一起,但系统会开始为"意图"争论不休。

event 描述某件已经发生的事:`OrderPlaced`、`PaymentCaptured`、`UserRegistered`。command 则请求某件事发生:`CapturePayment`、`SendEmail`。在架构里,不把这两种形式混用是划算的。

如果一个 event 被当成隐藏的 command 来用,consumer 就会依赖 producer 的内部意图。这会破坏 loose coupling(松耦合)。

## 2. 例子与反例

异步不该成为"错误悄悄消失"的地方。

### 例子:transactional outbox

> 书记员先把信登记进台账,然后才寄出。即便收发室宕了,这条记录也不会丢。

创建订单时,service 在同一个事务里写入订单和 outbox event。一个独立的 worker 读取 outbox,把 event 发布到 broker。如果 broker 暂时不可用,event 留在数据库里,稍后会被发出去。

outbox 堵上了一个经典的漏洞:数据已经存了,但 event 在 commit 和 publish 之间丢了。

### 反例:把队列当成垃圾桶

> 收件箱不是一个流程。如果没人去处理它,活儿只是被藏起来了。

团队把每一个重操作都丢进队列,却不定义 retry policy、幂等性、ordering、visibility timeout、DLQ 或告警。到了生产环境,消息要么卡住、要么重复、要么处理上好几个小时。

队列本身并不会让系统变可靠。它只是把复杂度从请求路径,挪进了后台处理。

#### 自检

- 用户现在就需要结果吗?
- 如果一条消息被投递了两次,会发生什么?
- "这个 event 必须被发布"这个事实, 存在哪里?
- 系统如何展示积压和失败的 job?

下一模块:[Performance](./software-architect-13-performance)。
