04 - 异步化与削峰
缓存解决了「读」的压力。这一章解决另外两个问题:主流程被慢任务拖累、突发流量打垮下游。核心工具就是你已经学过的消息队列。
一句话先记住:异步让主流程「秒返回」,削峰让系统「扛得住高峰」,代价都是「允许任务晚一点完成」。
(消息队列的原理见《消息队列入门》系列,这里只讲它在高并发优化里扮演的角色。)
4.1 异步化:把非核心任务从主流程摘出去
问题:主流程被拖慢
以「用户下单」为例,同步串行做完所有事:
下单接口(同步):
|── 写订单 ──|── 扣库存 ──|── 发短信 ──|── 加积分 ──|── 推荐更新 ──|
0.05s 0.05s 1.5s 0.1s 0.8s
总计 ≈ 2.5s ← 用户干等 2.5 秒
用户其实只关心「订单下成功了没」,发短信、加积分、更新推荐这些晚几秒完全无所谓。
方案:非核心任务丢进队列
下单接口(异步):
|── 写订单 ──|── 扣库存 ──|─丢消息─| → 立刻返回(≈ 0.1s)
0.05s 0.05s 0.001s
╰──▶ 消息队列
├─▶ 短信服务(后台慢慢发)
├─▶ 积分服务
└─▶ 推荐服务
用户等待从 2.5s 降到 0.1s,体验天差地别。
怎么判断能不能异步? 问一句:这件事,需要「现在立刻做完」才能给用户答复吗?
| 任务 | 同步/异步 | 原因 |
|---|---|---|
| 写订单主记录 | 同步 | 核心,必须成功 |
| 扣库存 | 同步 | 涉及一致性,要当场确认 |
| 发短信/邮件 | 异步 | 晚几秒无所谓 |
| 加积分、更新推荐 | 异步 | 后台算即可 |
4.2 削峰:用队列扛住突发流量
问题:峰值打垮下游
假设数据库每秒最多处理 200 个写请求。平时 100/s 岁月静好,秒杀一来瞬间 5000/s:
没有队列(直连):
5000/s 洪峰
│
▼
┌──────────┐
│ 数据库 │ 每秒只能处理 200
│ ⚡ 爆炸 │ ← 5000 瞬间压过来,直接崩
└──────────┘
方案:队列当「蓄水池」
有队列(削峰填谷):
5000/s 洪峰
│
▼
┌────────────────────┐
│ 消息队列(蓄水池) │ 瞬时洪峰先排队
│ [■■■■■■■■■■■■■■] │ 队列变长,但不崩
└─────────┬──────────┘
│ 消费者稳定按 200/s 取
▼
┌──────────┐
│ 数据库 │ 始终在能承受的速度内工作
└──────────┘
洪峰被「削平」,摊到后面一段时间慢慢消化(填谷)。代价是高峰期任务会延迟,但系统不崩。
4.3 异步化的两种实现方式
在 FastAPI 后端里,异步任务通常有两种做法:
| 方式 | 适合场景 | 特点 |
|---|---|---|
| BackgroundTasks(FastAPI 内置) | 轻量、和请求同生命周期的小任务 | 简单,但任务跑在同一进程,进程挂了任务就丢 |
| 消息队列 + 独立 Worker(如 Celery + RabbitMQ) | 重要的、耗时的、要保证不丢的任务 | 独立进程消费,可重试、可持久化、可水平扩展 |
经验:无关紧要的小任务(记个日志)用 BackgroundTasks 就行;重要任务(发短信、支付回调处理)必须走消息队列,保证「丢不了、可重试」。
4.4 异步削峰的代价
- 数据延迟:任务不是立刻完成的,用户可能「下单成功了但短信过一会儿才到」
- 一致性变弱:主流程返回成功 ≠ 所有后续任务都成功,需要处理「消息丢失/重复/失败重试」
- 系统更复杂:多了 Broker 和 Worker 要运维、要监控
什么时候用:任务「不需要实时完成」、或「流量有明显尖峰」、或「某服务不该拖垮主流程」时。反之,强一致、要即时结果的(如扣款结果),老老实实同步做。
本章小结
| 你要记住 | 要点 |
|---|---|
| 异步化 | 非核心任务丢队列,主流程秒返回 |
| 判断标准 | 「需要现在立刻做完才能答复用户吗?」不需要就异步 |
| 削峰 | 队列当蓄水池,洪峰先排队,消费者稳定消费 |
| 两种实现 | 小任务用 BackgroundTasks,重要任务用消息队列 + Worker |
| 代价 | 数据延迟、弱一致、系统更复杂 |
下一章预告:无论同步异步,频繁「创建连接、创建线程」都是巨大浪费。池化技术教你「复用」。
上一章 ← 03 - 缓存:高并发第一利器 | 下一章 → 05 - 池化技术