从同步到异步:任务队列如何削峰、解耦与保住主链路
文章发布时间:
最后更新时间:
最后更新时间:
很多链路一开始都写成同步,因为最直观。但只要业务动作越来越多,同步链路会逐渐变成一个“谁慢谁拖全场”的结构。
所以我看异步化,不是为了上消息队列而上,而是先识别哪些动作不该继续绑在主请求上。
一个典型场景
以“用户注册成功”这件事为例,主链路真正必须立刻完成的通常只有:
- 写用户主数据
- 返回注册结果
但现实里经常还会顺手做这些事:
- 发欢迎消息
- 写审计日志
- 发优惠券
- 通知风控
- 更新画像标签
如果全部同步做,任何一个慢点或失败点都可能把注册接口拖长。
主链路怎么拆
原则很简单:把“必须现在完成”和“可以稍后完成”分开。
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_registered、order_paid 这类明确事件,而不是“某张表更新了”这种模糊信号。
结果
一轮合理的异步化,通常应该带来这些结果:
- 主请求变短,尾延迟更稳定
- 非关键任务失败,不再直接拖垮核心接口
- 高峰时服务更容易靠队列缓冲住流量
- 问题定位从“同步链路一锅粥”,变成“投递、消费、补偿”几个清晰环节
总结
后台服务从同步走向异步,重点不是会不会用 MQ,而是能不能把边界讲清楚:哪些动作该异步、事件如何定义、重复消费如何兜底、失败之后谁来补偿。