2026-09-23·by Sijie Wang#node#project#youteacher#profile

talent-search-sync-queue

教师档案到搜索服务的同步

profile 服务持有教师的权威数据。talent 服务持有学校端查询的可检索索引。这是两个各自独立、各带自己存储的服务,所以档案的一次改动必须传播出去——当老师改了自己的标题、技能、或对谁可见时,这个改动得抵达搜索索引。本节讲的就是承载这次传播的那道接缝。

一个 port,两条路

应用层从不直接跟队列或 HTTP 客户端打交道。它只依赖一个 port——TalentSyncPort,整个接口面只有一个方法:sync(data)。这个 port 自己的注释把意图讲得很直白——具体实现可以是同步的(HTTP)也可以是异步的(队列)。所以领域层只说「这位老师的数据变了,把它送到搜索去」;至于走哪条路,是基础设施层的选择,可替换,且不必碰任何 use case。

它承载的载荷 TalentSyncData,是一份档案里与搜索相关的投影:身份信息(id、username、标题、简介、头像)、学校端用来筛选的各个 facet(技能、语言、城市/国家与期望地点、可入职状态、经验级别与年限、期望薪资区间、资质),以及——很关键的——可见性标志:一个 public/limited/hidden 级别,外加三个布尔值,分别表示该老师是否对学校、对雇主、对招聘方可见。可见性是随着同步一起走的,这样索引才能强制执行「谁被允许看见谁」。

队列这条路:跑在 Redis 上的 BullMQ

BullTalentSyncAdapter 是异步实现。它把消息发布到一个由 Redis 支撑、名为 talent-sync 的 BullMQ 队列,而它的任务选项才是有意思的地方——那是一份「持久性契约」:

  • 重试 3 次,采用指数退避,起始延迟一秒,这样下游的一次瞬时故障能自行重试,而不是把这次更新丢掉。
  • 完成即移除,健康的队列不会堆积已完成的任务。
  • 失败保留——一个把重试次数耗尽的任务不会被丢弃。它留下来供检查,让真正失败的同步是可见的,而不是悄无声息地消失。

每个任务拿到一个形如 talent-<id>-<时间戳> 的 id,发布时也会记日志。队列另一端的消费者是 talent 服务;profile 服务的职责只是入队。

Redis 连接本身是一个惰性创建的单例客户端,由环境变量配置,带有自己一层有上限的重试:若干次尝试、延迟递增且封顶,之后就对这一次请求放弃。于是这里叠了两层重试——连接层重试传输,队列层重试这份工作。

HTTP 这条路:尽力而为,不阻塞

TalentServiceClient 是同步的另一种实现——一次直接的对外调用,把教师记录 POST 到搜索服务的 upsert 端点,以一次经过认证的服务间请求发出。它最具定性的特征是它在失败时的做法:不做任何会让调用方停下的事。 4xx/5xx 响应会记日志;抛出的网络错误会被捕获并记日志。两者都不会被重新抛出。代码里把原因写得清清楚楚——同步失败不应阻塞主操作。保存档案才是那件必须成功的事;把它送进搜索索引固然可取,但地位次之,允许它在日志里大声地失败、对用户则悄无声息。

底下的设计

这两条路表达的是同一个一致的立场:档案是真相之源,索引是派生视图,让二者保持同步这件事绝不能把档案扣为人质。 队列这条路,为那次「追上进度」买来了持久性和自动重试;HTTP 这条路买来了即时性,但会退化为尽力而为。因为两者都躲在同一个单方法 port 之后,profile 服务在接线时挑选自己的取舍,而领域层始终不知道当下跑的是哪一个。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →