从同步到异步:任务队列如何削峰、解耦与保住主链路

文章发布时间:

最后更新时间:

很多链路一开始都写成同步,因为最直观。但只要业务动作越来越多,同步链路会逐渐变成一个“谁慢谁拖全场”的结构。

所以我看异步化,不是为了上消息队列而上,而是先识别哪些动作不该继续绑在主请求上。

一个典型场景

以“用户注册成功”这件事为例,主链路真正必须立刻完成的通常只有:

  • 写用户主数据
  • 返回注册结果

但现实里经常还会顺手做这些事:

  • 发欢迎消息
  • 写审计日志
  • 发优惠券
  • 通知风控
  • 更新画像标签

如果全部同步做,任何一个慢点或失败点都可能把注册接口拖长。

主链路怎么拆

原则很简单:把“必须现在完成”和“可以稍后完成”分开。

sequenceDiagram participant C as Client participant A as API participant M as MySQL participant Q as Queue participant W as Worker
C->>A: register request
A->>M: create user
A->>Q: publish user_registered
A-->>C: success
Q-->>W: consume event
W->>M: write audit / coupon / profile tasks

主请求只保留:

  • 校验参数
  • 写核心数据
  • 投递事件

剩下的通知、审计、积分、画像更新交给异步 worker。

异步化的收益

它至少解决三件事:

  • 削峰:高峰时把瞬时流量摊平
  • 解耦:主服务不需要同步依赖所有下游
  • 隔离:非关键能力失败,不直接打断主链路

但这不是免费午餐,代价是复杂度从“同步顺序调用”转移到了“消息可靠投递与消费治理”。

四个值得注意的方向

1. 幂等

异步消费最现实的问题不是“会不会成功”,而是“会不会重复成功”。

所以消费侧必须能接受重复消息,例如:

  • 基于业务主键去重
  • 基于事件 ID 记录消费状态
  • 用唯一索引兜底

2. 重试

重试不是简单 for 三次。要先区分:

  • 临时性错误:可重试
  • 参数错误:不该重试
  • 下游长期异常:需要熔断或人工介入

3. 死信与补偿

如果消息一直消费失败,不能无限重试把队列拖死。应该:

  • 进入死信队列
  • 记录上下文
  • 提供补偿或回放入口

4. 事件边界

不是所有数据库写操作都值得发一条消息。事件应该围绕业务语义,而不是表级别的机械变更。

我更喜欢像 user_registeredorder_paid 这类明确事件,而不是“某张表更新了”这种模糊信号。

结果

一轮合理的异步化,通常应该带来这些结果:

  • 主请求变短,尾延迟更稳定
  • 非关键任务失败,不再直接拖垮核心接口
  • 高峰时服务更容易靠队列缓冲住流量
  • 问题定位从“同步链路一锅粥”,变成“投递、消费、补偿”几个清晰环节

总结

后台服务从同步走向异步,重点不是会不会用 MQ,而是能不能把边界讲清楚:哪些动作该异步、事件如何定义、重复消费如何兜底、失败之后谁来补偿。

参考