消息队列与异步化
消息队列(Message Queue)是大型系统的「传送带」:秒杀(Lesson 0012)、通知系统(Lesson 0014)、feed 流(Lesson 0011)全都建立在它上面。面试深挖必问的投递语义与幂等,恰好也是工作中 MQ 事故的头号来源。
§1 · 先问一句哪些步骤必须同步?
异步化的第一步不是引入 MQ,而是切开「用户在等」和「用户不等」。以注册流程为例:
| 步骤 | 同步/异步 | 理由 |
|---|---|---|
| 校验 + 创建账号 + 发 token | 同步 | 用户在等结果,后续操作依赖它 |
| 发欢迎邮件 | 异步 | 晚 30 秒无所谓,失败也不该阻塞注册 |
| 初始化推荐画像 | 异步 | 后台任务,跟用户眼前的体验无关 |
| 记营销埋点 | 异步 | 纯旁路,丢了都能接受 |
把「用户不等」的步骤扔进队列,注册接口的耗时和稳定性立即改善。这就是 MQ 的第一价值:把慢的、脆的、跟主流程无关的东西摘出去。
§2 · MQ 的三大价值
- 削峰填谷(peak shaving):瞬时 10 万写请求 → 队列缓冲 → 消费者按自己节奏 5000/s 稳定处理。Lesson 0012 的秒杀全靠这一手。
- 解耦(decoupling):下单服务只管发「订单已创建」事件,谁要消费(发短信、加积分、更新报表)它不关心。新增下游 = 加一个消费者,主服务零改动。
- 缓冲重试:下游挂了,消息留在队列里等它恢复,天然的重试缓冲区。
§3 · 代价投递语义与幂等
天下没有免费的队列。三种投递语义(Kafka 文档的经典划分[1]):
| 语义 | 含义 | 代价 |
|---|---|---|
| 至多一次 at-most-once | 可能丢,绝不重 | 消费前崩溃 → 消息没了(适合埋点等可丢场景) |
| 至少一次 at-least-once | 绝不丢,可能重 | 消费者必须幂等(工业界默认选择) |
| 精确一次 exactly-once | 不丢不重 | 性能代价大、条件苛刻(Kafka 事务 + 幂等生产者) |
「至少一次」意味着重复消费是常态而非异常(消费者处理完还没提交 offset 就崩溃,重启后重收同一条)。所以消费者必须幂等(idempotent):同一条消息处理 100 遍,效果跟 1 遍一样。标准手段:消息带唯一 ID,消费前查去重表;或数据库写入用「INSERT ... ON CONFLICT」类幂等写法。Stripe 的幂等 API 设计是绝佳范本[2]。
另外三个要知道的坑
- 顺序:Kafka 只保证分区内有序。要「同一用户的操作有序」→ 按 user_id 做分区键(Lesson 0006 的哈希思想)。全局有序基本不存在于高吞吐系统。
- 毒丸与死信(DLQ):一条消息格式坏掉,无限重试会卡死整个分区。重试 N 次失败 → 扔进死信队列,人工处理,主流程继续。
- 积压(lag):消费速度追不上生产速度。lag 是 MQ 最重要的监控指标——它等于「系统的债务」,只会越滚越大。
MQ 的代价:排查链路变长、最终一致引入心智负担、运维一套新系统。同步能解决且规模可预期的,不要上 MQ。面试中主动说「这个场景我不建议用 MQ,因为……」是高级信号。
§4 · 随堂检测
§5 · 检索练习
凭记忆做两件事:① 把「支付成功」后的流程拆成同步/异步两列(至少 5 个步骤);② 写出「至少一次」语义下消费者保证幂等的两种手段。
核对参考答案
① 同步:更新订单状态、扣减库存确认;异步:通知(短信/推送)、积分发放、物流创建、报表更新、营销触达。② 消息唯一 ID + 去重表(消费前查);幂等数据库写(upsert / 条件更新 / 唯一索引兜底)。
§6 · 本周行动
① 在你公司系统里找一个「本可异步却在同步链路里拖着接口变慢」的步骤(发通知、写日志、同步第三方是高发区);
② 如果已有 MQ:消费者是幂等的吗?lag 监控在哪看?有没有配 DLQ?
③ 反过来找一个「明明该同步却扔进 MQ」的步骤——让用户无感知等待变成不确定等待。
§7 · 延伸资源
- 首选精读:Kafka 官方 Introduction + 文档中「Delivery Semantics」节[3]
- DDIA「Stream Processing」章(第 1 版第 11 章)——事件流的理论全景
- Stripe 幂等请求设计——幂等键的工业级范例[2]
- 下一课 → Lesson 0008 大案例 I:短链服务完整版(把 L1–L7 全部串起来)