定时触发器只该把小块、可重复的工作塞进队列,长跑的发货扇出交给队列工作进程去扛。真正卡住系统的不是计时器本身,而是当某个工作进程、网络调用或订阅方在半路失败时,怎么让重试不造成二次伤害。

15分钟执行上限会改变整个系统的形状

打开网易新闻 查看精彩图片

与其让一个调度进程一直开着,逐个联系诊所、药房和患者端应用,不如让调度器只发布有边界的小任务,让工作进程从持久化状态里恢复执行。我给每次订阅方投递都配了一条发件箱记录和一个幂等键。这对组合不如精巧的定时表达式那么炫,但它能扛住重启。

in_transit 这样的发货状态看起来只是一个事件。可在医疗健康工作流里,它可能扇出到几十个订阅方,每个订阅方的超时时间和鉴权策略都不一样。如果定时进程内联发送通知,一个慢端点就会把后面所有投递都卡住。这时候一旦重试,前面已经发过的消息就可能被重复发送。

更安全的单位是一条投递记录

这条记录包含:发货ID、订阅方ID、事件版本、尝试次数、下次尝试时间,还有一个稳定的去重键。调度器找出到期记录并入队。工作进程认领一条记录,发送更新,然后把对应版本标记为已投递。如果在标记之前崩溃,就会触发重试;因此接收方也必须把这个键当作幂等键来处理。

实际使用中,这条记录本身就是支持工程师需要的审计轨迹。当护理团队说某条更新到晚了,它应该能告诉你:选中了哪个事件版本、哪次尝试持有租约、请求什么时候离开你的系统,以及响应是被接受、被拒绝,还是超时后状态未知。我见过一些团队只保留一个最终的已发送布尔值,然后花几个小时从散落的日志里拼时间线。对于重复消息可能让患者困惑、或者触发第二次下游动作的工作流来说,这是笔很差的交易。把状态转换显式保留下来,留足历史来解释它们,让保留策略——而不是图省事——来决定旧的投递行什么时候离开数据库。

短事务会帮上忙。

别用进程内的 Set 当去重存储

它在部署时就会消失,而且两个 Node.js 工作进程可能同时观察到同一个键不存在。把唯一性规则放进持久化存储里,比如在 (shipment_id, subscriber_id, event_version) 上建唯一约束,然后把写入和入队决策显式分开。

让定时回调保持无聊:查一批有限的数据,把消息入队,记录一个入队时间戳,然后退出。队列工作进程负责退避和可见性续期。这样 15 分钟上限就只是一个批量大小的信号,而不是整份报告或整个扇出的截止时间。

投递记录的类型可以定义成:id、shipmentId、subscriberId、eventVersion、dedupeKey。入队函数先按 limit 和 pending 状态查出到期行,再逐条处理。整个思路的核心不是把定时任务写得更聪明,而是把容易失败的长链路拆成可以安全重试的小单元。