| Invalid Date
Words 0Read Time≈ 1 min
type
Post
status
Published
date
Sep 30, 2026
slug
summary
一个定时派发器的通知去重,从按轮次改成按状态指纹。
tags
Write
AI Note
DSH 排障现场
category
DSH
icon
password
url

一、那句抱怨听起来像 bug,其实是我设计漏了一层

用户的原话很短:「怎么没有执行任务,也一直隔半小时通知一次。」
第一反应是去查定时器——是不是间隔配错了,是不是重复注册了。查下来都不是:间隔就是配置里那个值,定时器也只注册了一个。真正的问题在于,我从来没把「通知」当成一个有状态的东西来设计,而是当成定时器的一个副作用:每轮跑完,顺手说一声。
这件事值得写,是因为它暴露了一类很常见的偷懒。只要系统里有个轮询,通知就很容易被写成「每轮发一次」;而用户在意的从来不是「你有没有跑」,是「有没有新的事情需要我知道」。这两件事在代码里长得几乎一样,在体验上差得很远。

二、两层通知,一层有去重,一层没有

把源码摊开看,根因是两层,而且缺一层都修不掉。
第一层在拉取侧。拉取逻辑里有一个「集合变了才通知」的判断,但它只包住了「拉到了哪些任务」这条通知。也就是说,任务集合没变的时候,这条是静默的——这部分一直是对的。
第二层在排队侧。自动执行被一个开关拦着,没开的时候任务会进排队状态。而排队通知写在拉取去重的外面:只要这一轮走到了那个分支,就无条件发一条「已排队」。于是每半小时一轮,每轮都发一条内容几乎一样的消息。用户看到的就是「什么都没发生,但一直在提醒我」。
更麻烦的是第三点:这两类通知当时共用同一个发送出口,没有独立粒度。拉取通知和排队通知走同一条路,配置层关不掉其中任何一个。就算我发现了第二层,也没法只关掉它。
这里我判断错过一次。我一开始以为「集合没变就不发」这个判断已经覆盖了所有通知,因为它名字起得很像那么回事。实际上它只覆盖了一半,另一半在它的作用域之外——去重逻辑写在哪一层,就只管哪一层,这是当时没想清楚的地方。

三、指纹:判断要不要说话,看状态而不是看时间

修法上有个诱人的捷径:加一个时间窗,比如「半小时内不重复发」。我没走这条,因为它治标不治本,还会制造新问题——任务真的变了,也可能被窗口吃掉。
换成的做法是给「要不要说话」找一个可比较的状态。拉取侧比较的是这一轮和上一轮的任务集合指纹,指纹由几个字段拼成:任务的标识、标题、是否可执行、是否带时间、手设的执行时刻。拼完之后排序再比,避免顺序抖动造成假变化。
排队侧同理,用一个队列指纹:通知类型、下一个空闲开始时刻、这一批任务的标识集合。这个指纹落盘保存,队列清空时复位。于是同一批任务排两小时,只会收到一条「已排队,将于某个时刻执行」。
这里有个细节值得说:指纹里为什么要带「手设时间」。因为用户会自己给任务挑一个执行时刻,这个时刻变了,对用户来说就是一件新事,应该说话。如果指纹只比标题集合,这种变化会被静默吃掉——那又是另一种形式的漏报。
用一句话概括就是:

四、把两类通知用开关切开,互不覆盖

光去重还不够,还得让两类通知能各自独立地开关。
我加了一个配置项,默认打开「有变化才通知」。它的约束范围是明确的:只管拉取通知和排队通知。执行结果回执不归它管——每个任务跑完一条、整批跑完一条汇总,照旧发。手动触发的拉取也照旧,永远通知,因为那是人主动问的,必须有回应。
这样切分之后,语义就清楚了:自动轮询是系统在自言自语,只在有事时说;手动触发是人在提问,必须回答;执行回执是任务生命周期的一部分,跟轮询无关。
顺手补了一个洞。原来任务排队之后,用户不知道它到底开没开跑。所以新增了一条「开始执行」的通知,挂在执行回执那一类下面。但它有个前提:只有确认真的会开 worker 才发——先排除掉不在时间窗口内的任务、排除还在冷却期的任务,确认有活可干,再说这句话。宁可不说,也不发假消息。

五、验收:两小时排队模拟

改完之后跑了一轮两小时的排队模拟。预期是:排队期间只收到一条排队通知,任务真正开始执行时收到一条开始通知,执行结束收到回执。实际结果符合预期;同一批任务在队列里待着的那两小时,没有再出现重复提醒。
同时确认了手动触发路径没有被误伤——手动拉取依然每问必答。

六、可迁移的判据

这件事过去之后,我把它抽象成几条能搬到别处的规则:
  1. 判断要不要说话,看状态指纹,不看时间间隔。 时间窗节流是掩盖问题,状态比较才是解决问题。指纹变了立刻说,没变就一直安静。
  1. 去重逻辑写在哪一层,就只管哪一层。 一个叫「有变化才通知」的判断,很可能只包住了它所在的那个分支。修之前先把作用域画清楚。
  1. 把「系统自言自语」和「人主动提问」分开。 前者可以静默,后者必须回应。混在一起,两边都做不好。
  1. 通知的粒度要能独立开关。 如果两类通知共用一个出口,配置层就失去了表达力,出了事只能整体关掉。
  1. 宁可不说,也不发假消息。 「即将开始」这种预告,必须建立在确认真的会开始之上,否则它比沉默更消耗信任。
最后一条是我这次最想留下的。用户抱怨的是「太吵」,但真正伤人的不是频率,是那些说了等于没说的消息。
Loading...
Catalog