从同步到异步:任务队列如何削峰、解耦与保住主链路
文章发布时间:
注册请求如果还要同步发消息、写审计日志、更新画像,下游任一环节变慢,都会拉长接口耗时。异步化要先划清边界:哪些操作决定注册是否成功,哪些可以稍后完成。
一个典型场景
以“用户注册成功”这件事为例,主链路真正必须立刻完成的通常只有:
- 写用户主数据
- 返回注册结果
但现实里经常还会顺手做这些事:
- 发欢迎消息
- 写审计日志
- 发优惠券
- 通知风控
- 更新画像标签
如果全部同步做,任何一个慢点或失败点都可能把注册接口拖长。
主链路怎么拆
原则很简单:把“必须现在完成”和“可以稍后完成”分开。
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 这类明确事件,而不是“某张表更新了”这种模糊信号。
怎么判断改造是否有效
改造后要比较主请求延迟,也要确认失败事件能被发现和补偿。预期效果包括:
- 主请求变短,尾延迟更稳定
- 非关键任务失败,不再直接拖垮核心接口
- 高峰时服务更容易靠队列缓冲住流量
- 问题定位从“同步链路一锅粥”,变成“投递、消费、补偿”几个清晰环节