webhook 的 fire-and-forget:别让同步拖垮主流程

架构与工程实践37 阅读约 4 分钟

「文章保存后同步到 AI 知识库」这种需求,最容易写出一种危险的代码:在保存逻辑里同步等 Webhook 发完、等下游返回成功,才算保存成功。看着很「严谨」,其实是给系统埋了颗雷。

反面教材

// ❌ 别这么写
async create(dto: CreateArticleDTO) {
  const article = await this.articleModel.create({ ...dto })
  await this.aiService.syncToKnowledgeBase(article)  // 同步等待,致命
  return article
}

问题在那个 await

  • AI 服务慢,用户就跟着慢:发文章的响应时间被下游拖累;
  • AI 服务挂了,文章就发不出去:一个「锦上添花」的功能,硬生生变成了主流程的单点故障;
  • 跨境网络抖一下,保存就失败:把不可控的外部依赖塞进了核心写路径。

主流程被一个副作用绑架了。

正解:fire-and-forget

「同步到 AI」是副作用,不是主流程的一部分。它应该「发出去就不管」——成功最好,失败也绝不能影响文章保存:

// ✅ 主流程只管自己的事,副作用通过事件异步触发
async create(dto: CreateArticleDTO) {
  const article = await this.articleModel.create({ ...dto })
  this.eventEmitter.emit(EVENT_ARTICLE_CHANGED)
  this.eventEmitter.emit(EVENT_ARTICLE_UPSERTED, article.toObject())
  return article            // 立即返回,不等任何下游
}

create 只负责「把文章存进数据库」这一件主流程的事,然后广播一个事件就返回。监听这个事件的 Webhook 发送器在「主流程之外」异步去同步 AI,它的成败完全不回流到 create

发送端也做了防御:

  • 异步、不阻塞:发 Webhook 不挡主流程;
  • 失败不抛回主流程:下游错误自己消化掉(记日志),不冒泡;
  • 可禁用:密钥未配置时整个同步关闭,本地开发不依赖 AI 服务;
  • 有兜底:万一某次同步丢了,另有全量重灌接口可手动补。

这是一个普适原则

「主流程 vs 副作用」的解耦不止用于这一处。判断标准很简单——问一句:

这件事失败了,用户的核心操作该不该失败?

  • 「文章存进数据库」失败 → 发文章必须失败(主流程);
  • 「同步到 AI」「发通知」「更新搜索索引」「打点统计」失败 → 发文章不该失败(副作用)。

凡是答案为「不该」的,都应该 fire-and-forget:用事件 / 消息队列 / waitUntil 之类的手段从主流程里剥离,让它们在旁路异步执行、独立失败、独立重试。

一致性怎么办

有人会担心:异步了,会不会出现「文章存了但 AI 没同步上」的不一致?会,短暂地。但这是可接受的最终一致性——AI 知识库晚几秒甚至偶尔漏一条,远比「发不出文章」轻。对这类副作用,我们要的是最终一致,不是强一致。再配一个兜底重灌接口,就足够稳。

小结

发 Webhook 同步 AI 是副作用,主流程(文章入库)绝不能为它阻塞、更不能为它失败。用事件把副作用从主流程剥离,做到 fire-and-forget:异步、失败不回流、可禁用、有兜底。判断法则就一句——「这件事失败了,用户的核心操作该不该跟着失败?」答案为否的,统统赶出主流程。

相关文章

评论 (4)

M

Mia

讲得比官方文档好懂多了

G

Gavin

是的,生产上我就是这么做的,踩过坑才敢这么写 😄

蝈蝈

fire-and-forget 别拖垮主流程,血泪教训啊

老李头

可观测性这块小项目常被省掉,其实最该有