定时任务的那些隐藏陷阱

作者:键盘上的猫 发布时间: 2025-12-04 阅读量:21 评论数:0

有天早上我被电话叫醒,说昨晚的对账结果全是错的。原因是:凌晨 2 点的对账任务那天跑了两次。

为什么跑两次?因为那天是夏令时切换日,某个时区的凌晨 2 点出现了两遍。

这是我职业生涯里最哭笑不得的一个 bug,也是我开始认真对待定时任务的起点。在那之前我一直觉得定时任务最简单——不就是到点执行一段代码吗。

陷阱一:时间本身就不可靠

还有几种类似的:

  • 夏令时前跳:某些时区凌晨 2 点直接跳到 3 点,2:30 当天不存在,配在这个点的任务不会执行。
  • 服务器时钟漂移:NTP 出问题时机器可能慢几分钟。我遇到过一台慢了 90 秒,它认为「还没到执行时间」,实际早过了。
  • 容器时区不一致:镜像默认 UTC,宿主机是本地时区。任务配在「凌晨 2 点」,实际跑在早上 10 点——正好业务高峰。我们踩过,还挺严重。

我现在的做法是:服务器和容器统一用 UTC,调度表达式也按 UTC 写,只在展示给人看时转换时区。

陷阱二:上一次还没跑完,下一次又启动了

任务配的是每 5 分钟一次,平时跑 30 秒。某天数据量暴涨跑了 20 分钟——于是 4 个实例同时跑,争抢同一批数据,把数据库锁死了。

多数调度框架默认不做这个保护,需要显式开启并发控制;自己写的话就加分布式锁:

if not redis.set(lock_key, worker_id, nx=True, ex=1800):
    log.info('上一轮尚未结束,跳过本次')
    return

这里有个细节:锁的过期时间要大于任务最长可能耗时,否则任务还在跑锁就没了,下一轮照样并发。但设太长,进程崩溃后要等很久。我的折中是稍长的过期时间加运行期续期。

陷阱三:集群里每台机器都跑了一遍

把 crontab 写在部署脚本里,服务扩容到 5 个实例,于是每个任务同时跑 5 遍。这错误特别常见——单机时代它完全正常,扩容那天才暴露。

解法:用支持分布式调度的框架,或者用上面那个锁,或者把定时任务剥离出去单独部署、副本数固定为 1。我们选第三种,因为它的资源画像和在线服务差别很大——吃内存、跑得久、对延迟不敏感。

陷阱四:时间窗口用了 now 而不是「上次成功时间」

这是逻辑上最微妙的一个。假设任务每小时跑一次,处理过去一小时的数据:

start = now - 1 hour
end = now

看着没问题。但某一次执行失败了呢?那一小时的数据就永久丢失了,因为下一次只处理它自己的那一小时。

正确做法是维护一个「水位线」——记录上次成功处理到哪个时间点,每次从那里继续:

start = 读取水位线()
end = now - 5分钟   # 留一点缓冲,避免处理到还在写入的数据
处理(start, end)
更新水位线(end)

这样失败一次,下次自动补上。

注意那个 5 分钟缓冲。直接用 now 会漏掉「时间戳已生成但事务还没提交」的记录。这个坑我踩过,表现是对账偶尔少几条,毫无规律。

陷阱五:只在失败时告警

我们有个任务默默停了 11 天没人发现。调度器本身出了问题,任务根本没被触发——没执行就没有失败,没有失败就没有告警。

没有消息不等于好消息。 现在每个任务成功后上报一次心跳,监控侧检查「超过预期周期的 2 倍还没心跳」就告警。这叫死人开关,比失败告警重要得多。

陷阱六:忘了幂等

任务会重试、会被手动重跑,就必须能安全跑第二遍。「给昨天下单的用户发券」不做幂等,重跑一次就是双倍成本。我们给每次发放生成业务唯一键,数据库加唯一索引,重复插入直接跳过。

一个检查清单

我现在新建一个定时任务,会过一遍这几个问题:

  1. 它在集群里会执行几次?
  2. 上一次没跑完,这一次会怎样?
  3. 失败了,数据会不会永久丢失?
  4. 重跑一次,会不会产生副作用?
  5. 它一直不跑,多久能被发现?
  6. 它的日志和输出去了哪里?
  7. 数据量涨十倍,它还跑得完吗?

七个问题,两分钟就能过一遍。但我敢说能全部答上来的定时任务不超过一半。

评论