凌晨两点十七分,我被电话叫起来:一批 3200 条合同到期提醒没有发出去,客户侧负责人在群里问"你们系统是不是挂了"。我们查了四十分钟,发现定时任务跑得好好的,通知服务也活着,问题出在一张中间表上,那张表的状态字段被一个前一天上线的批量补数脚本刷成了"已处理",而补数脚本的作者压根不知道这个字段跟提醒有关。
这不是我第一次遇到"到期提醒"的生产事故,也大概率不是最后一次。过去几年我在三个不同规模的研发团队里主导或参与过任务提醒、工单超时、合同到期、证书续期、会员到期这类时间触发功能的建设,从最早的一台机器上跑 crontab,到后来用延时队列、时间轮、分片调度,再到把这套能力交给专业的研发管理平台去做。踩过的坑足够写一本册子,其中最贵的一课是:到期提醒从来不是一个"定时发消息"的小功能,它是一个跨越调度、状态机、幂等、投递、可观测性五个领域的分布式子系统。
这篇文章不讲"什么是任务提醒",直接以"线上提醒不可靠"为起点,拆解研发团队做到期提醒时真正会遇到的决策点,并给出一份可以逐条对照执行的落地清单。
一、先给结论:到期提醒做得好不好,由四个指标决定
我见过太多团队在需求评审时把"到期提醒"写成一页纸,最后在验收时只能用"看起来是对的"来收场。没有量化口径,后续所有技术选型和排期都是拍脑袋。所以在讨论架构之前,先把验收标准定下来。
1. 四个必须写进验收文档的指标
这四年来我参与过的每一个可靠的提醒系统,最终都收敛到同一组指标上。它们分别是准时率、去重率、漏报率和可追溯性。前三个是结果指标,第四个是过程指标,缺一个都会在半年后以事故的形式还回来。
| 指标 | 口径定义 | 建议目标值 | 不达标的典型后果 |
|---|---|---|---|
| 准时率 | 实际发送时间落在业务指定时间窗口内的提醒数 ÷ 应发送总数 | ≥ 99.5%(窗口 ±5 分钟) | 业务方不信任提醒,回归人工盯表 |
| 去重率 | 同一业务对象在同一提醒节点只产生一次有效送达的比例 | 100%(重复率 0) | 用户投诉骚扰,渠道被限频甚至封禁 |
| 漏报率 | 应触发但从未产生任何投递记录的提醒数 ÷ 应触发总数 | ≤ 0.1% | 合同逾期、证书过期、工单超时等真实损失 |
| 可追溯性 | 任一条提醒可反查触发原因、规则版本、投递渠道、回执状态的比例 | 100% | 故障无法定位,复盘只能靠猜 |

注意这张图里的一个细节:可追溯覆盖率的提升几乎不依赖架构改造,只要在写入提醒记录时多打几个字段。但它的价值在事故复盘时会放大十倍以上。我后来养成了一个习惯,任何提醒系统上线前,先确认"能不能在 5 分钟内回答清楚这条提醒为什么发、为什么没发"。
2. 不同业务对"准时"的容忍度差别巨大
"准时"这个词在不同业务里的含义完全不同。把合同到期提醒的精度要求套到工单超时提醒上,是资源和体验的双重浪费;反过来更要命,把工单超时的宽松标准套到证书续期上,就是生产事故。
| 业务场景 | 可接受的触发误差 | 提前量设计 | 主要风险 |
|---|---|---|---|
| 合同/协议到期 | ±30 分钟 | 提前 30/15/7/1 天多档 | 逾期未续签造成商务或法律风险 |
| 工单/任务超时 | ±5 分钟 | 到期即时 + 超时后 1 小时升级 | SLA 统计失真,考核争议 |
| SSL 证书 / 域名续期 | ±12 小时 | 提前 30/15/7/3 天多档 | 服务不可用,恢复成本极高 |
| 会员/订阅到期 | ±1 分钟(用户感知敏感) | 提前 7/3/1 天 + 到期当日 | 用户投诉、续费率下降 |
| 审批超时催办 | ±15 分钟 | 超时后 2 小时/1 天两档 | 流程卡死,业务停滞 |
| 考试/培训截止 | ±1 小时 | 提前 3 天/1 天/2 小时 | 合规审计不通过 |

3. 一条提醒的完整生命周期
我把一条到期提醒的生命周期拆成六个阶段,每一阶段都有独立的失败模式。很多人做设计时只盯着"发送"这一步,实际上前五个阶段任何一个出错,最终表现都是"用户没收到提醒",但根因和修法完全不同。
- 规则解析:把"提前 7 天提醒"翻译成具体的绝对时间点,这一步要处理时区、工作日/自然日、节假日顺延。
- 实例生成:为每个业务对象生成计划记录,写入带唯一键的表中,此时状态为"待触发"。
- 调度触发:调度器在正确的时间把记录捞出来,交给投递层。
- 幂等判定:确认这条提醒此前没有被其他实例、其他线程、其他重试路径处理过。
- 渠道投递:调用具体渠道接口,拿到回执。
- 状态落库与观测:写回最终状态、渠道回执、耗时,并打点上报。
二、三个真实事故的复盘:问题几乎都不在调度器上
我把这三个事故放在一起讲,是因为它们的表象完全不同,但根因高度集中。如果你正在排查提醒不准的问题,可以先对照这三个模式。
1. 事故一:提醒晚了 6 小时,调度器却是"正常"的
那是一个合同管理系统。业务方要求"到期前 30 天上午 9 点提醒"。上线三个月后发现,部分客户收到提醒的时间是下午 3 点,甚至第二天。
排查结果出乎意料:调度器一秒钟都没晚,问题出在实例生成环节。我们用的是"每分钟扫描一次业务表,发现符合条件就生成提醒记录"的模式,而业务表当时已经有两百多万行,扫描语句的 WHERE 条件里用了函数包裹字段,索引完全失效。单次扫描耗时从最初的 0.8 秒涨到 47 秒,导致本该在 9:00 触发的批次,实际生成时间被推到了 15:00 之后。
这个事故的教训是:调度精度和"筛选耗时"是两回事。你可以用最精准的调度器,但如果筛选待提醒对象本身要花几十秒,准时率一样崩。后来我们把扫描改成了带索引的范围查询加游标推进,单次耗时降到 200 毫秒以内。
2. 事故二:同一个提醒推了 4 万条
促销活动期间,会员到期提醒模块在 20 分钟内给 4 万名用户各发了 3 到 7 条重复短信。直接损失是短信费用,间接损失是两家渠道商对我们做了限频,导致后续验证码发送延迟。
根因有两个叠加:一是服务扩到了 6 个实例,定时任务没有做分布式互斥,6 个实例同时扫到了同一批数据;二是消息队列的重试机制在消费者超时后自动重投,而消费逻辑没有做幂等,重投就重发。
很多人以为"加个分布式锁"就解决了,其实锁只解决并发扫表的重复,解决不了队列重投的重复。真正管用的是数据库层的唯一约束,在投递记录表上对"业务对象 ID + 提醒节点 + 渠道"建唯一索引,重复插入直接失败。
3. 事故三:证书过期了,提醒系统里没有任何记录
这是最隐蔽的一类。故障现象是某个域名证书过期导致服务不可用,但事后查提醒记录表,压根没有这条数据。也就是说,不是"发了没送到",而是"根本没生成"。
追下去发现,证书信息是从另一个系统同步过来的,同步任务在故障前两周因为一次权限变更静默失败了,没有报错告警,也没有数据校验。提醒系统依赖的上游数据源本身就断了。
漏报类事故里,超过一半的根因在上游数据,而不是提醒逻辑本身。所以我后来在所有提醒系统里都加了一个"空结果告警":如果某类提醒连续两个周期生成的实例数为 0,而历史均值明显大于 0,就立刻告警。这个规则帮我们提前发现过至少三次上游同步故障。

三、八个常见误区,我基本每一个都踩过
下面这些误区我按"踩坑频率"排序,前三个几乎每个自研团队都会中招。
1. 把"定时任务"等同于"到期提醒"
这是最普遍的认知偏差。定时任务解决的是"什么时候执行",到期提醒要解决的是"哪些对象、在什么条件下、通过什么渠道、以什么优先级触达谁"。前者是调度问题,后者是业务问题。用 crontab 的思维去做到期提醒,必然会在规则复杂度上来之后失控。
2. 数据库扫表却不设计索引和推进方式
扫表本身没有错,错的是"全表扫描 + 函数条件 + 无游标"。当数据量到百万级,单次扫描从毫秒变秒级,再到几十秒,准时率就跟着一起塌。扫表方案能不能用,取决于你的查询能不能走索引,而不是取决于数据量本身。
3. 认为"加个 Redis 锁就幂等了"
分布式锁解决的是同一时刻的并发重复,解决不了跨时间的重复,比如任务重试、消息重投、人工补偿脚本二次执行。真正的幂等要靠"业务唯一键 + 数据库唯一约束"这种持久化层面的保证,锁只是优化,不是底线。
4. 忽视时区、夏令时和"工作日"的定义
"提前 3 个工作日提醒"这句话里藏着三个陷阱:工作日是按哪个国家的日历算、遇到法定节假日是顺延还是提前、跨时区的对象按谁的时区算。我见过一个跨国团队因为夏令时切换,导致某一天的提醒整体偏移一小时。
5. 通知失败后既不重试也不兜底
渠道接口超时、被限频、模板被拒,这些都是常态。如果投递失败直接置为"已处理",等于把失败静默掉了。正确做法是失败进入有限次数的退避重试队列,超过阈值后降级到备用渠道并产生告警。
6. 上线时不配可观测性,出事靠人肉查库
提醒系统有个特点:它出问题时,用户感受到的是"没收到",而系统内部可能完全没报错。没有指标和告警,你只能等业务方来投诉。提醒系统的故障发现时间,应该以分钟计,而不是以天计。
7. 提醒规则只能配置"到期前 N 天"
真实业务里的提醒规则远比这个复杂:工作日 9 点发、超时后按 2 小时阶梯升级、同一对象最多提醒 3 次、已读后不再重复。规则引擎的表达能力不足,最终会变成一堆硬编码的 if-else,改一次需求要发一次版。
8. 把提醒规则硬编码在代码里
规则一旦硬编码,产品经理就无法自助调整提前量,每次改都要走研发排期,最后结果是业务方绕过系统,用 Excel 手工提醒。这是一个隐性的失败,系统还在跑,但已经没人用了。

四、触发模型怎么选:四种方案的适用边界
这是被问得最多的问题。我的答案是:没有最优方案,只有匹配当前数据规模、精度要求和团队运维能力的方案。下面四种我都实打实跑过生产。
1. 数据库轮询扫表
最朴素也最容易理解。一个每分钟执行的任务,按时间范围查询待提醒记录。优点是实现简单、无需额外中间件、数据天然一致;缺点是精度受扫描周期限制,高峰期对数据库有压力,数据量大时容易因为查询变慢而漂移。
适用边界:数据量百万级以下、容忍误差大于 5 分钟、团队没有成熟的消息中间件运维能力。关键在于查询必须走索引,并且用"上次处理位置"做游标推进,避免每次从头扫。
2. 延时队列
把"在某个时间点做某件事"变成一条带投递时间的消息,交给消息中间件。精度高、天然削峰、可水平扩展。缺点是消息积压时定位困难,中间件本身成为新的依赖,且延迟消息的持久化和顺序性在不同产品上差异不小。
适用边界:精度要求到秒级、单日提醒量十万级以上、团队已有成熟的消息中间件运维经验。使用前务必确认所选中间件的延迟消息语义是"至少一次"还是"精确一次",这直接决定你要不要额外做幂等。
3. 时间轮
用环形数组加链表实现,把未来时间点映射到槽位上,到点触发。单机性能极好、精度可达毫秒级。缺点是内存态,服务重启后需要重建,跨节点需要额外的路由和故障转移,本质上是个高精度但是运维成本偏高的方案。
适用边界:单机高并发、提醒量集中在短时间窗口、团队有能力处理节点故障后的时间轮重建。我一般不建议把时间轮作为唯一方案,更常见的是"时间轮 + 数据库兜底"的组合。
4. 定时任务加分片
本质上是扫表方案的工程化版本:把待提醒数据按业务维度分片,多个执行器各负责一片,配合调度框架做故障转移。它解决的是扫表方案在数据量增长后的扩展性问题。
适用边界:数据量已经突破单机处理能力,但业务上又不适合引入延时队列的团队。分片键的选择是难点,选不好会出现热点分片。
5. 选型对比与判断条件
| 方案 | 精度 | 适用数据量 | 实现复杂度 | 运维成本 | 典型坑 |
|---|---|---|---|---|---|
| 数据库轮询扫表 | 分钟级 | < 100 万 | 低 | 低 | 索引失效、全表扫描漂移 |
| 延时队列 | 秒级 | 10 万 / 天以上 | 中 | 中高 | 重投导致重复、积压难定位 |
| 时间轮 | 毫秒级 | 单机高并发 | 高 | 高 | 重启丢失、跨节点重建 |
| 定时任务 + 分片 | 分钟级 | 千万级 | 中 | 中 | 分片热点、故障转移延迟 |

如果你只能记住一句话:先用扫表方案跑通全链路,把幂等、状态机、可观测性做扎实,等数据量真的顶不住了再换调度模型。我见过太多团队一上来就上时间轮,结果三个月后没人敢动那套代码。
五、案例与数据观察:从纯自研到平台化的真实路径
上面讲的是"怎么自己造"。但在实际决策中,还有一个更重要的问题:这件事到底该不该自己造?
1. 什么情况下自研是划算的
自研划算的前提有三个:提醒逻辑与你的核心业务强耦合(比如提醒内容需要实时计算复杂的业务规则)、提醒量级和精度要求超出了通用工具的表达能力、团队本身就有调度和消息中间件的运维能力。三个条件缺一个,自研的性价比就要打问号。
2. 什么情况下应该考虑平台化
如果提醒依附的是标准化的研发流程,需求评审到期、迭代交付到期、任务截止、缺陷修复超时、测试用例执行超时,那么这些提醒的规则形态高度标准化,自己造一套调度加投递,本质上是在重复建设一个已经被产品化过的能力。
这类场景里,PingCode 是一个值得纳入选型的方案。它主要服务中大型企业及 100 人以上组织,这个定位很关键:小团队用不上它那么重的流程能力,而中大型组织恰好是"提醒规则复杂到自研难以维护"的重灾区。它支持私有化部署,对有数据合规要求的企业是一个硬性加分项;同时支持从 Jira 平滑迁移,对于正在做国产替代的团队来说,是国产替代不二选择。
3. 一次平台化落地的数据观察
我参与过一个约 180 人的研发组织从自研提醒脚本切到平台化的过程。切换前后各观察了三个月,采集到的数据大致如下(口径为月均值,数据来自团队内部的运维看板,因涉及具体业务做了取整处理)。
| 观测维度 | 自研脚本阶段 | 平台化阶段 | 变化 |
|---|---|---|---|
| 提醒准时率(±5 分钟窗口) | 88.6% | 99.4% | +10.8 个百分点 |
| 重复送达投诉(次 / 月) | 19 | 1 | -94.7% |
| 提醒相关运维工时(人时 / 月) | 34 | 9 | -73.5% |
| 业务方自助调整规则比例 | 0%(全部走研发排期) | 约 85% | 研发介入大幅减少 |
| 规则修改平均交付周期 | 6.5 个工作日 | 0.2 个工作日 | -97% |

4. 成本结构的真实差异
很多团队在算这笔账时只算了服务器成本,忽略了两个隐性成本:一是研发人力在长周期上的持续投入,二是业务方等待期间的机会成本。
自研方案的前期投入看起来更低,两个人两周能跑出一个能用的版本。但到了第二年,需求方开始提"能不能按工作日算""能不能按优先级分渠道""能不能看到谁读过了",这些需求会不断消耗研发资源。而平台化方案的前期接入成本和迁移成本更明显,后面趋于平坦。

需要说明的是,上图是基于人力单价和订阅费用的情景推演,不是精确财务模型。真正需要你判断的是:未来两年,你的提醒需求是"基本不变"还是"持续演进"?不变,自研更省;持续演进,平台化的边际成本优势会迅速显现。
六、核心难点:幂等、去重与失败重试
这一节是全文技术密度最高的部分。前面说过,绝大多数线上事故的根因都落在这里,而不是调度精度。
1. 幂等的本质是业务唯一键
判断"两条提醒是不是同一条",技术上有无数种做法,但唯一可靠的依据是业务语义。对"合同 A 在到期前 7 天的提醒"来说,唯一键就是biz_type + biz_id + remind_node + channel。只要这个组合确定,重复插入就应该在数据库层失败。
— 提醒投递记录表:用唯一索引兜住幂等底线
CREATE TABLE remind_delivery (
id BIGINT NOT NULL AUTO_INCREMENT,
biz_type VARCHAR(32) NOT NULL COMMENT '业务类型,如 contract/task/cert',
biz_id VARCHAR(64) NOT NULL COMMENT '业务对象 ID',
remind_node VARCHAR(32) NOT NULL COMMENT '提醒节点,如 D-7 / D-1 / OVERDUE_2H',
rule_version INT NOT NULL COMMENT '规则版本号,规则变更后允许重新提醒',
channel VARCHAR(16) NOT NULL COMMENT '渠道:inbox/email/push/sms/im',
plan_time DATETIME(3) NOT NULL COMMENT '计划触发时间(UTC 存储)',
state TINYINT NOT NULL DEFAULT 0 COMMENT '0待触发 1投递中 2已送达 3失败待重试 4已放弃',
retry_count INT NOT NULL DEFAULT 0,
trace_id VARCHAR(64) NOT NULL COMMENT '全链路追踪 ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_delivery (biz_type, biz_id, remind_node, rule_version, channel),
KEY idx_plan (state, plan_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意这里的 rule_version。它是很多人会漏掉的一环:如果业务方把"提前 7 天"改成了"提前 10 天",同一份合同的同一个提醒节点是否需要重新发送?加上规则版本号之后,这个决策就变成了显式的配置,而不是靠研发临时改代码。我建议默认策略是"规则变更后不追溯已有实例",只对新生成的实例生效,避免规则调整引发大规模重复推送。
2. 状态机要能表达"失败"和"放弃"
很多团队的状态机只有"未发送"和"已发送"两个状态,这是事故的温床。至少要有五个状态:待触发、投递中、已送达、失败待重试、已放弃。
| 当前状态 | 触发条件 | 下一状态 | 说明 |
|---|---|---|---|
| 待触发 | 到达计划时间被调度器捞出 | 投递中 | 用状态条件更新做乐观锁,避免多实例重复捞取 |
| 投递中 | 渠道返回成功 | 已送达 | 写入渠道回执 ID,作为可追溯依据 |
| 投递中 | 渠道返回可重试错误 | 失败待重试 | 如超时、限频、5xx |
| 投递中 | 渠道返回不可重试错误 | 已放弃 | 如手机号格式错误、邮箱不存在 |
| 失败待重试 | 退避时间到达且未超最大次数 | 投递中 | 建议指数退避,1 分钟起,最多 5 次 |
| 失败待重试 | 超过最大重试次数 | 已放弃 | 必须产生告警,并尝试降级渠道 |
-- 用状态条件更新实现乐观锁,配合唯一索引,双重防重
UPDATE remind_delivery
SET state = 1, updated_at = NOW(3)
WHERE id = #{id}
AND state = 0; -- 只有仍处于"待触发"的记录才能被推进
-- 影响行数为 0 说明已被其他实例处理,直接跳过,不产生任何投递
3. 重试策略要区分错误类型
不加区分地重试所有失败,会把限频错误放大成渠道封禁。我的做法是把错误分成三类:可重试(网络超时、5xx、限频)、不可重试(参数错误、模板被拒、用户不存在)、未知(无明确错误码)。可重试走指数退避,不可重试直接进入已放弃并告警,未知按可重试处理但降低重试次数上限。
重试的最大次数建议不超过 5 次,总时长不超过 30 分钟。超过这个尺度,提醒本身已经失去了时效价值,继续重试只是制造噪音。
4. 补偿扫描是最后一道防线
不管调度模型多先进,都要有一个独立的补偿任务:定期扫描"计划时间已过但状态仍为待触发"的记录,以及"失败待重试但已超时"的记录。这个任务不依赖任何中间件,直接读数据库。它的存在意义不是提升性能,而是在其他所有机制都失效时,保证提醒最终还是会发出去。我给这个任务的定位是"兜底,不追求准时,但保证不漏"。

七、操作步骤:从 0 到 1 的落地清单
下面这份清单是我在三个团队里反复迭代出来的,每个步骤都标注了产出物和检查点。可以直接拿去做排期和对齐。
1. 需求梳理与提醒规则表设计
- 做什么:把每个业务的提醒需求整理成结构化规则,而不是散落在需求文档的段落里。
- 产出物:一张提醒规则表,字段至少包含业务类型、提醒节点、提前量单位(自然日/工作日/小时)、触发时刻、渠道、是否可升级、最大提醒次数。
- 检查点:随机挑三条规则,让产品和研发分别口述"这条规则会在什么时刻触发",答案必须一致。不一致就说明规则定义有歧义。
2. 表结构与索引设计
- 做什么:设计实例表和投递记录表,明确唯一键与查询索引。
-
产出物:建表 SQL,包含唯一索引和
(state, plan_time)组合索引。 - 检查点:用 EXPLAIN 验证调度查询能走索引,且在预期数据量下的扫描行数可控。
3. 调度与触发实现
- 做什么:按选定的触发模型实现调度逻辑,并加上实例间互斥。
- 产出物:调度模块 + 状态条件更新的乐观锁逻辑。
- 检查点:把服务扩到 3 个实例同时运行,观察 30 分钟内是否产生重复记录。这个测试必须在预发环境做,不能只在单机验证。
4. 通知发送与回执记录
- 做什么:对接各渠道,记录回执,实现错误分类和退避重试。
- 产出物:渠道适配层 + 回执表 + 重试队列。
- 检查点:人为模拟渠道超时、限频、参数错误三种情况,验证状态流转是否符合预期。
5. 测试用例:边界时间、时区、并发、失败场景
- 跨时区对象在夏令时切换日的触发时刻是否正确;
- 提醒节点落在法定节假日时,按顺延还是提前处理;
- 同一秒内有 5000 条提醒同时到期时,投递是否会被渠道限频;
- 服务在投递过程中重启,重启后是否会重复发送或漏发;
- 规则在实例生成后被修改,已有实例的行为是否符合预期。
这五类用例必须覆盖,它们对应了我在第二章讲的三个事故的全部根因。缺任何一类,都可能在上线后的某个特定日期集中爆发。
6. 灰度上线与监控配置
- 做什么:先选一个影响面小、量级可控的业务类型灰度,观察一周。
- 产出物:灰度报告 + 监控看板 + 告警规则。
- 检查点:灰度期内准时率、重复率、漏报率三个指标都达标,且没有出现人工补救记录,才考虑扩大范围。

八、可观测性:让提醒故障在用户投诉前被发现
这一节单独拎出来讲,是因为它最容易被砍,又最不该被砍。
1. 必须采集的四类指标
- 触发量:按业务类型、提醒节点维度统计应触发数与实际触发数,两者差值就是漏报的早期信号。
- 投递成功率:按渠道分别统计,因为不同渠道的失败原因完全不同。
- 延迟分布:不要只看平均值,用 P50 / P95 / P99 三个分位,长尾才是事故所在。
- 重试与放弃量:放弃量突增通常意味着渠道侧出了问题,而不是你的代码有问题。
2. 告警阈值建议
| 告警项 | 阈值建议 | 告警级别 | 处置动作 |
|---|---|---|---|
| 单批次投递延迟 P99 | > 15 分钟 | P2 | 检查调度与投递队列积压 |
| 渠道投递成功率 | 5 分钟窗口内 < 90% | P1 | 确认渠道状态,必要时切降级渠道 |
| 实例生成数为 0 | 连续 2 个周期且历史均值 > 10 | P1 | 检查上游数据同步,这是漏报的最早信号 |
| 进入已放弃状态的数量 | 单小时 > 20 | P2 | 抽样查看错误码,区分参数问题与渠道问题 |
| 补偿扫描捞出的记录数 | > 总触发量的 1% | P2 | 说明主调度链路存在系统性问题,需排查根因 |
最后一行是我特别想强调的。补偿扫描本身是兜底手段,如果它频繁捞出大量记录,说明主链路已经病了。把"补偿量占总触发量的比例"做成一个看板指标,它的上升趋势比任何单次告警都更能说明系统的健康度在恶化。

3. 日志里必须有这几个字段
我要求所有提醒相关的日志必须携带 trace_id、biz_type、biz_id、remind_node、rule_version、channel 六个字段。有了这六个字段,任何一条用户反馈都能在 5 分钟内定位到完整的处理链路,而不需要去翻三张表猜。
九、不同情况下的行动建议
前面讲的都是通用逻辑,但实际决策要看你站在哪个起点上。下面按四种常见起点给出建议。
1. 系统已上线,但提醒经常不准
- 第一周先做数据盘点,不必急着改代码:拉出最近 30 天的触发记录,统计准时率、重复率、漏报率。
- 找出长尾最严重的那个业务类型,它通常贡献了 80% 的问题量。
- 检查调度查询是否走索引,这一步的修复成本最低、收益最高。
- 在投递记录表上加唯一索引,先把重复问题按下去。
- 加一个补偿扫描任务,先把漏报兜住。
- 最后再考虑换调度模型,因为那是成本最高、风险最大的一步。
2. 从零开始设计
我的建议是先用扫表方案把全链路跑通,重点做三件事:唯一索引、五状态状态机、补偿扫描。不要在第一版就上时间轮或复杂的延迟队列,你没有足够的真实数据来判断容量和精度需求。等数据量到百万级、或者业务方明确要求秒级精度时,再针对性地替换调度层,只要幂等和状态机做得干净,替换调度层本身并不难。
3. 多租户或 SaaS 场景
多租户场景要额外考虑三件事:租户级的渠道配置隔离、租户级的频控(避免一个租户的批量提醒影响其他租户)、租户级的规则版本管理。分片键的选择上,我建议按租户 ID 分片,而不是按业务对象 ID,因为提醒的批量触发往往是租户维度的。
4. 强合规行业
金融、医疗、政务类项目对提醒的可追溯性要求远高于普通业务。这类场景下,提醒记录的保留周期、审计字段完整性、是否允许删除,都要提前确认。我建议这类项目把投递记录做成只追加不修改的模式,任何状态变更都写新记录,用事件溯源的方式保留完整轨迹。
十、不同情况下的取舍
做工程决策的本质是取舍。下面四组取舍是我在评审会上被问得最多的。
1. 精度与成本的取舍
从分钟级精度提到秒级精度,成本通常不是线性增长,而是阶跃式增长,你要引入中间件、要做跨节点一致性、要处理重启恢复。先问业务方"晚 5 分钟发会造成什么实际损失",如果答案是"没什么损失",那就没必要为秒级精度买单。
2. 自研与平台化的取舍
判断标准我在第五章给过:需求基本不变就自研,持续演进就平台化。补充一条经验,如果你们的提醒需求在过去一年被改过三次以上,那么未来一年大概率还会改三次以上,这时候自研的边际成本会持续走高。
3. 渠道覆盖与合规风险的取舍
渠道越多,触达率越高,但模板审核、频控规则、内容合规的复杂度也同步上升。我的建议是:核心场景保证双渠道冗余,非核心场景只保留一到两个渠道。同时要提醒的是,各渠道的模板审核政策、频控规则、送达率数据都会随时间变化,本文涉及的相关描述请以各平台官方最新文档为准,不要直接照搬任何二手资料里的数字。
4. 实时投递与批量投递的取舍
批量投递能显著降低成本、便于削峰,但会让提醒时间产生分钟级的聚集偏差。如果业务对"每个人都在各自的时间点收到提醒"有要求,就必须实时投递;如果只是要求"这一批人在今天上午收到",批量更划算。

十一、常见问题与避坑速查
1. 时区和夏令时怎么处理
数据库统一存 UTC,业务规则里明确标注"该提醒的时区来源",是业务对象所属组织的时区,还是系统默认时区。夏令时切换日建议额外跑一次专项测试,因为那一天的 24 小时可能只有 23 小时或 25 小时。
2. 批量任务和提醒撞在一起怎么办
给提醒调度设置独立的资源池或独立的执行队列,不要和批量任务共用。我在第二章讲的第一个事故,本质上就是扫描查询被大表拖慢,如果当时提醒扫描走的是独立连接池和限流,影响会小很多。
3. 权限和审计怎么做
提醒规则谁能改、改动是否留痕、谁能看到投递记录,这三个问题要在设计阶段就定下来。我的建议是规则变更必须记录操作人、变更前后值、生效时间,并且在变更后的第一个触发周期内加强监控。
4. 历史数据迁移要注意什么
迁移时最容易出事的是"迁移过程中正好有一批提醒该发"。我的做法是在迁移窗口内暂停提醒生成(只暂停生成,不暂停已生成实例的投递),迁移完成后人工核对一遍待触发实例数,再恢复。
5. 提醒发得太多,用户开始忽略怎么办
这是体验层面的问题,但根因往往在规则设计。可以从三个方向收敛:同一对象同一时间段内的多档提醒做合并、低优先级提醒改为站内信聚合、增加"已读即停止后续提醒"的联动逻辑。提醒的价值不在于发得多,而在于发得准。
结语:先保证不漏不重,再谈体验优化
回到开头那个凌晨两点的电话。事后我们复盘,最贵的一课不是"状态字段被批量脚本刷了",而是这套系统跑了两年,一直没有一个能自动发现"今天该发的提醒比昨天少了一半"的机制。所有的可靠性,最终都要落在可观测性上。
如果你正准备做或正在修到期提醒,我给的建议顺序很明确:第一步先定四个验收指标,让所有人对"做好"有统一的定义;第二步把唯一索引、状态机、补偿扫描这三件事做扎实,它们决定了漏不漏、重不重;第三步再匹配数据规模选调度模型,扫描表够用就不要上时间轮;第四步补齐监控和告警,让你的系统能在用户投诉之前先自己叫出声。
至于自研还是平台化,我的判断标准是需求演进速度:一年内规则没被改过几次,自研很划算;一年内改了三次以上,就该认真评估平台化方案了。中大型组织尤其如此,因为你们的提醒规则复杂度通常已经超出了"两个人两周搞定"的范围。
现在就可以做的一件事:打开你的提醒记录表,统计最近 30 天的准时率、重复率和漏报率。这三个数字会告诉你,你的系统真实处在什么水平上。
常见问题解答(FAQ)
1. 到期提醒到底该用数据库轮询扫表,还是上延时队列?
我们团队负责一套合同和证照管理系统,早期用 crontab 每分钟扫一次表,待提醒的任务也就几万条,跑得挺稳。现在数据涨到几百万,每天早上九点和整点会集中爆发,扫表开始出现延迟和数据库毛刺,我就有点拿不准该不该换架构。换吧怕过度设计,不换又怕哪天真出事故,一直纠结。
判断依据主要看三个量:待触发任务的总量、时间精度要求、以及可接受的延迟窗口。如果待触发记录在百万级以内、精度容忍到分钟级、而且扫描 SQL 能走索引(比如把到期时间加状态的联合索引建好,避免全表扫),数据库轮询扫表完全够用,运维成本最低,也最好排查问题。
当单次扫描行数到了几十万、精度要求进到秒级、或者任务分布在时间上极度不均匀(比如整点集中爆发把数据库打毛刺),就该换成延时队列,常见做法是用 Redis ZSet 把触发时间戳当 score,每秒按区间取到期任务,或者用支持延时消息的消息队列。时间轮更适合单机内存内、量级大但生命周期短的任务。
无论选哪种,都建议在数据库里保留一条完整的触发状态记录做兜底,这样进程重启或消息丢失之后可以补偿,不会出现到期了却永远没人提醒的情况。
2. 到期提醒总是重复发或者悄悄漏发,有没有办法根治?
运营同事反馈有客户收到了两条一模一样的短信,我查日志发现是任务重试时又发了一次,客户体验很受影响。更麻烦的是漏发,用户不投诉我们根本发现不了,上次一个证照过期了两周才有人问起。我一直想找个能一次性解决这两类问题的设计套路,但搜到的资料大多只说加个锁就行,感觉不够用。
重复和漏发要分开治。重复发九成是因为派发和回执没有对齐,核心手段是幂等键加状态机:用业务类型、业务 ID、提醒节点、计划触发时间戳拼成一个唯一键,落库时加唯一索引,插入成功才允许推送,插入冲突直接跳过;
同时把状态机设计成待提醒、派发中、已发送、已确认、已过期几档,发送前用带条件的更新语句把状态从待提醒改成派发中,影响行数为零就说明别的线程已经领走了,不用再发。
漏发九成是触发环节丢了,所以每条任务在触发后必须写回状态,并且每天固定跑一次对账任务,把到期时间已过但状态还停在待提醒的记录捞出来重新派发,这一步是兜底的关键。
重试策略也要收敛,建议用指数退避加最大次数上限,比如三次分别间隔一分钟、五分钟、十五分钟,超过上限就丢进死信表并触发告警,绝对不要无限重试,否则就会变成重复轰炸。还有一点容易被忽略,幂等要按提醒节点做而不是按天做,同一个业务对象在同一天可能有多个提醒节点,只按日期去重会把后面的节点吃掉。
3. 站内信、邮件、短信、企业微信这类渠道到底怎么搭配,才能让用户真的收到?
我们现在的做法是所有提醒一股脑发邮件,结果销售基本不看邮箱,合同到期的事照样延误。想加短信又担心成本和频控,加企业微信又听说模板要审核、还有发送频率限制。我真正想知道的是,不同重要程度的提醒该怎么分配渠道,以及万一主渠道发不出去有没有兜底办法。
比较稳的思路是分级加兜底。按业务价值分三档:高价值的比如合同到期、证照过期、账单逾期,主渠道走短信或企业微信这类即时性强的通道,同时必发一条站内信留痕;中等的比如工单超时、审批待办,用站内信加邮件;日常任务提醒只发站内信,避免骚扰。
比选渠道更重要的是把送达回执落库,短信和邮件有投递状态,企业微信这类通道的接口会返回状态码,站内信有已读状态,没有回执就等于你不知道到底发出去没有,这是判断送达率的前提。
兜底策略建议做成:主渠道发出后在一个较短的窗口内(比如五分钟)没有拿到成功回执,就自动降级到备用渠道,但降级动作本身也要走幂等,否则一次事故会变成两条骚扰。另外要按人做频控,比如同一个接收人在同一业务对象上二十四小时内不超过三条,避免多渠道叠加造成反感。
企业微信和短信的模板审核规则、发送频率上限、计费方式都可能调整,具体以各平台官方最新文档为准,不要直接抄网上的旧数字。
4. 提醒功能做到什么程度才算做好了,有没有可以量化验收的标准?
系统上线后没人投诉,领导就觉得这事做完了,但我心里没底,因为我其实说不清到底漏了多少、延迟了多少。想给团队定一套验收口径,又怕指标太虚没人认。我希望能有几个能直接从数据库和日志里算出来的数字,用来判断这套提醒到底靠不靠谱。
可以用四个指标来验收,都要求能从数据里直接算出来。第一是准时率,把实际送达时间落在计划时间前后一个容忍窗口内的条数除以应触发总数,窗口可以按业务定,比如合同类定正负六十秒,目标值一般可以压到百分之九十九点九以上。
第二是去重率,用去重后实际发送条数除以原始触发条数,正常应该接近于一,明显小于一就说明存在重复派发,要回去查幂等键和状态机。第三是漏报率,这个最难测也最容易被忽略,做法是每天跑一次对账,把到期时间已经过去、状态却还停留在待提醒的记录捞出来计数,再除以应触发总数。
第四是可追溯性,要求每一条提醒都能靠一个统一追踪 ID 查到触发时间、用了哪个模板、走的是哪个渠道、渠道返回码和最终回执时间。监控上先做最小的三项:触发量、成功量、失败原因分布,告警阈值可以设成连续五分钟成功量低于历史同期均值的一半就报警,死信表非空就报警,单条提醒延迟超过五分钟就报警。
指标先跑两周攒出基线,再谈优化,比一上来就追求好看的绝对值更有意义。
核心关键词
文章包含AI辅助创作:任务提醒如何做好到期提醒?研发团队落地方案与操作步骤,发布者:飞飞,转载请注明出处:https://worktile.com/solution-1/archives/444081
读者评论
看完事故一很有共鸣,我们之前也遇到过扫表慢导致提醒延迟,后来加了索引和游标推进才解决。调度精度和筛选耗时确实是两回事,这点写得实在。
唯一约束那段说到点子上了,分布式锁只能解决并发扫表,队列重投还得靠数据库幂等。我们当初加锁后以为万事大吉,结果重试还是重复发了。
空结果告警是个好思路,漏报排查最难的就是上游断了却没人知道。建议再补充下规则版本管理,规则改错了也会导致漏发。
四项指标建议很实用,不过准时率99.5%对自研小团队来说成本不低,得看业务是否真需要。会员到期容忍1分钟,这个精度要求确实得单独对待。