后台任务与一致性:六个定时任务、幂等键与一次补偿

六个定时任务每月执行一百八十次,按百分之零点四的失败率算出零点七二次;一次时区切换让续费跑了两遍,二十个用户被扣两次。本文把幂等键、三次退避重试、死信表、事务性发件箱与日终对账的成本逐项算清,并给出八条可判定的验收动作。

定时任务最难的不是跑起来,是同一件事只跑一次。

一、时区切换那天,续费跑了两遍

2026 年 3 月最后一个周日,欧洲用户所在时区进入夏令时。陆远在凌晨两点部署了一次小修复,续费任务因为等待支付网关超时被平台重试;他看到后台只完成了一半,又手动点了一次「立即执行」。第二天早上,二十个用户发来邮件:同一笔订阅被扣了两次。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

问题不在重试,也不在手动执行,而在于这个任务没有「这件事只能做一次」的标记。任务名只写了 renew,参数是「今天」,而「今天」在应用服务器、数据库和支付网关那里并不总是同一天。

平台的自动重试只保证「再执行一次」,它不知道这一次和上一次属于同一笔业务;那个手动按钮也一样,它只传了任务名,没有传业务日期。

定时任务的可靠性,取决于副作用能否被识别为重复。 能重跑不等于能安全重跑:前者是调度器的事,后者是业务键的事。调度器只能保证「再试一次」,业务键才能保证「再试一次也不多扣钱」。

失败率低不代表风险低。 每月零点七二次失败听起来可以接受,但失败落在清理临时文件上只是磁盘涨一点,落在续费上就是用户被扣两次。平均失败率会掩盖这种不对称。

补偿不是重跑,是把已经发生的副作用改回来。 重跑适合「什么都没发生」的失败;一旦扣款已经成功,补偿就必须是退款或延期,而不是再执行一遍。

二、六个任务,两种代价

六个任务都挂在同一台机器的 cron 上,每天各触发一次,合计每月一百八十次。它们的差别不在代码量,而在失败之后要做什么。

任务频率幂等键失败后果补偿方式
每日对账每天 1 次recon:2026-10-26差异留到次日次日重跑并人工核对
订阅续费每天 1 次renew:2026-10-26扣款成功但订阅未延期幂等去重后退款
试用到期提醒每天 1 次trial:2026-10-26用户收到两封相同邮件发件箱去重
清理临时文件每天 1 次cleanup:2026-10-26磁盘占用上涨次日再跑
重算统计每天 1 次stats:2026-10-26报表滞后一天重跑覆盖写
发送周报每天 1 次(周一发送)weekly:2026-W43漏发或重发手动补发

表里只有前两个任务碰钱,也只有它们需要「补偿」而不只是「重跑」。这不是说另外四个可以随便写,而是说注意力应该按后果分配。每次执行都要在执行日志里留一行:任务名、幂等键、开始与结束时间、结果;失败时先看这个键有没有成功过,再决定是重跑还是补偿,而不是凭记忆判断。

按后果分级,不按任务数量平均用力。 六个任务各花同样的时间做幂等,是平均主义;把续费和对账做到可对账、可退款,其余四个做到不重复发信,才是按代价分配。

幂等键用业务日期,不用执行时间。 renew:2026-10-26 表示「十月二十六日这个业务日的续费」;写成带时分秒的时间戳,每次重试都会生成新键,等于没有键。键上要加唯一索引,让数据库而不是代码来保证只写一次。

周报的键用 ISO 周号。 任务每天触发,但只有周一真正发送;键写成 weekly:2026-W43,周二即使被手动补跑,也会命中同一周而复用已有结果。幂等键表只需要三列:键、状态、完成时间;状态从 running 走到 done,唯一索引建在键上,插入冲突就说明同一业务日期的这次执行已经有人在做。

三、重试、死信与发件箱

重试要先分类:网络超时、连接被重置、支付网关返回 5xx,可以重试;参数错误、余额不足、签名校验失败,重试多少次都是同一个结果,应该直接进死信表。

次序等待累计等待动作
第 1 次重试1 分钟1 分钟原样重跑,幂等键不变
第 2 次重试5 分钟6 分钟原样重跑,幂等键不变
第 3 次重试25 分钟31 分钟原样重跑,幂等键不变
超过 3 次——写死信表并告警

退避是给下游恢复时间,不是给自己省事。 1、5、25 分钟合计三十一分钟,足够覆盖一次数据库主从切换或一次支付网关抖动;间隔太短的重试只是把同样的失败重复三遍。

死信表不是垃圾桶,是待办列表。 每条死信要有任务名、业务日期、最后一次错误和负载;日终对账时扫一遍未处理条目,超过二十四小时未清零就告警。

发件箱把「写业务」和「发消息」拆成两步。 一张 outbox 表加一个轮询任务,轮询间隔三十秒;业务事务只写 outbox 行,发送失败不会回滚业务,业务失败也不会留下已经发出的消息。

日终对账每天跑一次,耗时九十秒,比对订单、订阅与支付回调三张表。差异大于零就告警,不做自动修复——自动修复会把「不确定」变成「悄悄改错」。

重放死信时仍然使用原来的键,不生成新键;否则「重放」会变成「第二笔业务」,这正是事故当天发生过的事。

告警内容里必须带幂等键和最后一条错误。只写「任务失败」四个字,凌晨看到仍然要登机器翻日志,那这条告警就只起到了叫醒作用。

六个任务的执行日志保留三十天就够。真正要长期留的是死信与对账差异,它们记录的是「钱有没有错」,不是「任务有没有跑」。

四、复算:一年 2.3 小时与一次 12 美元

任务数量:6 个 × 每天 1 次 = 6 次/日
任务执行:6 × 30 天 = 180 次/月
失败次数:180 × 0.4% = 0.72 次/月
年度失败:0.72 × 12 = 8.64 次/年
重复执行:8.64 × 40% = 3.46 次/年
单次人工成本:40 分钟 = 0.67 小时
人工处理:3.46 × 40 分钟 = 138 分钟 = 2.3 小时
重复扣款损失:20 × 15 美元 × 4% = 12 美元
道歉与退款沟通:20 × 5 分钟 = 100 分钟
重试总等待:1 + 5 + 25 = 31 分钟
日终对账:90 秒/日 × 365 = 32,850 秒 = 9.1 小时/年
对账占架构预算:9.1 ÷ 156 = 5.8%
对账与人工之比:9.1 ÷ 2.3 = 4.0

二点三小时一年,看起来不值得为它建一套机制。但这是「有幂等键、重复执行不产生副作用」之后剩下的成本;没有键时,重复执行会直接变成退款、道歉和手工改数据。

十二美元的退款也不多,一百分钟的道歉信也不多。真正贵的是之后三个月里,陆远每次看到续费任务失败都要犹豫「要不要手动点一下」——这个犹豫会拖慢每一次发布。

把二点三小时摊到全年一百五十二次改动上,每次不到一分钟;但一次重复扣款的恢复要走支付渠道,周期以天计。按小时算它小,按恢复时间算它大。

三十一分钟的退避窗口要和发布窗口错开。如果三次重试正好落在部署重启期间,它们会一起失败并进死信,直到第二天对账才发现。

如果只做一件事,先做幂等键而不是对账:键是事前阻止,对账是事后发现;前者把事故挡在发生前,后者只能把发现时间从一周缩短到一天。

对账自身每年花九点一小时,比对账能省下的二点三小时更贵。它的价值不在省时间,而在把「用户已经多付了钱而你不知道」变成「当天就知道」;这笔账不能只用小时数衡量。

五、行动清单与复算验收

  • 给六个任务各写一个「任务名:业务日期」的幂等键,并在数据库加唯一索引。
  • 把续费与对账标为 P0,其余四个标为 P1,P0 的失败必须当天有人看。
  • 在任务入口先查幂等键,命中即返回,不重复执行任何副作用。
  • 配置 1、5、25 分钟三次退避,第四次失败直接写死信表并告警。
  • 建死信表,字段至少包含任务名、业务日期、最后错误与负载。
  • 把「扣款成功但订阅未延期」写成对账 SQL,差异大于零就告警。
  • 给邮件与 webhook 加事务性发件箱,轮询间隔三十秒。
  • 每季度挑一个任务做重复执行演练,确认第二次执行的副作用为零。

验收标准:连续三十天里,同一业务日期的续费任务被触发多次时重复扣款为零笔,且死信表未处理条目在二十四小时内清零。

这八条里,前三条是零成本的,只需要一次决定;后五条要写代码,但加起来不超过两天。先做前三条,就能把最贵的那类事故挡掉。

清单的顺序就是实施顺序:先键、再分级、再重试与死信,最后才是发件箱。跳过前面的步骤直接建发件箱,只会得到一张更长的待办表。

使用边界与实操验收

任务少于三个时不要引入调度框架。 cron 加一张幂等键表就够;多引入一个调度框架,就是多一个要升级、要排查、要读文档的依赖,而它解决的问题你还没有。

要求跨行强一致的场景不适用。 「扣余额并记流水」这类操作应该在同一个数据库事务里完成;对账只能兜底,不能替代事务,也不要把对账当成事务的替代品。

对账不能替代幂等。 对账发现差异时,用户已经被多扣了一次;它的价值是把「不知道错了」变成「知道错了」,而不是让错误不发生。

与数据迁移、适配层的边界要分清。 03 管结构怎么变,08 管外部服务的形状怎么包,09 只管「同一件事被执行两次」;三份检查项不重叠,也不互相替代。

什么时候该停下来。 当你开始为定时任务写第二个调度器,或者为死信表再建一套工单系统时,停下来——一个人维护不了两套,第二套会在半年内失修。

关键参考与核验范围

核验日期:2026-10-26。以下文档用于核对概念与规则,本文所有数字均取自全组教学基线,不取自这些文档。

引用口径。 上述文档只用于核对模式名称、去重键位置与事务边界;文中的一百八十次、零点七二次、二点三小时与十二美元全部来自全组教学基线,不取自这些文档。

核验范围。 上述四个入口在 2026-10-26 实测可公开访问(HTTP 200);未核验各项目的最新版本、价格与配额。

如何自行复核。 取自己最近三十天的任务执行记录,数出执行次数与失败次数,用同一组算式替换一百八十与百分之零点四,再检查失败里有多少次是同一业务日期的重复执行。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元