几乎所有业务系统里都少不了定时任务:对账、数据同步、过期状态清理、报表统计、缓存刷新。很多开发的理解很简单,写好业务函数,配一个 cron 表达式,丢进调度框架,就可以不管了。直到好几次线上任务出问题,我才意识到定时任务的坑大多不在业务代码本身,而是调度层面的隐性问题。
最早踩的一个坑是任务重复执行。当时用的是单机调度,后面为了高可用,直接多机部署服务。没有任何分布式锁控制,到了调度时间点,所有实例同时触发任务。对账脚本一次性跑多遍,对账记录重复生成,统计数据翻倍,后续花了很久清理脏数据。那时候想当然觉得多实例只是容灾,完全没考虑调度器本身是独立触发的,没有互斥逻辑。
很多人解决重复执行,会加分布式锁,但锁的用法也容易出错。常见错误是锁只加在业务逻辑入口,锁的过期时间比任务执行时间短。如果任务执行很慢,还没跑完锁就自动释放,第二个实例拿到锁,又开始跑同一套逻辑,照样重复。反过来锁过期时间设置得无限长,任务中途崩溃没释放锁,任务直接卡死,再也不会执行。锁的超时时间,必须结合任务最大预期耗时,还要有兜底的释放策略。
和重复执行对应的另一个问题,是任务丢失。分布式调度里,如果触发时刻服务全部重启或者短暂不可用,这个时间片的任务直接跳过,不会自动补发。比如凌晨两点跑的日对账,刚好这个时间段在发布,服务下线,当天对账直接漏掉。很多调度框架默认没有补发机制,开发也不会特意去关注 "错过的调度",等到第二天核对数据才发现缺口。
还有任务堆积的问题,这个隐蔽性很强。cron 表达式是固定时刻触发,它只保证到点发起,不关心上一轮任务有没有跑完。比如一个清理任务,预期 5 分钟跑完,配置每分钟执行一次。某天数据库慢查询导致单次任务跑了 8 分钟,新的调度请求持续进来,任务实例越堆越多,线程池被占满,数据库连接被耗尽,最后连锁影响其他定时任务和同步接口。
不少框架会提供 "禁止并发执行" 的开关,很多人不知道这个选项。就算开了串行控制,也要区分两种策略:丢弃本次触发,还是等上一轮结束之后立刻执行。不同业务需求要分开选。对账类任务,重复跑风险大,优先丢弃;数据同步类,错过之后必须补,就不能直接丢,要记录缺口,后续补偿。
时区问题也是定时任务高频翻车点。服务器是 UTC 时区,开发在本地东八区写 cron,直接按北京时间配置。上线之后,任务触发时间整整差 8 小时。还有夏令时切换的场景,虽然国内没有夏令时,但如果服务部署在海外节点,很容易出现调度时间漂移,甚至某一小时任务消失或者连续执行两次。cron 表达式本身是不带时区信息的,时区由调度器所在环境决定,这点很容易被遗忘。
还有任务执行时长的观测盲区。大部分团队只配了执行失败告警,任务正常跑完就没有消息。任务慢慢变慢,从几十秒涨到十几分钟,这个过程没有告警提醒,等到某天超时、堆积了才发现。定时任务的监控,不能只看成功失败,还要采集每次的开始时间、结束时间、执行耗时、本次处理数据量。连续几次耗时持续上涨,就是预警信号。
依赖顺序也是一个容易踩的坑。有些任务有先后依赖,先跑 A 同步基础数据,再跑 B 基于 A 的数据做统计。大家习惯靠错开 cron 时间来控制顺序,比如 A 凌晨 1 点,B 凌晨 1 点半。这种方式非常脆弱,一旦 A 任务执行超时,跑到 1 点 40 才结束,B 已经先跑了,拿到不完整的数据,统计结果出错。靠时间差做依赖,本质上是弱约定,没有强保障。
另外还有资源挤占的问题。定时任务经常选在业务低峰期,比如凌晨。但低峰不代表可以无限制消耗资源。大报表导出、全表扫描清理这类重任务,一旦写得粗放,凌晨把数据库 IO 打高,会影响跨时区的海外用户、内部后台查询。定时任务也需要限流、分批处理,不能一次性全量拉取数据。
踩过这些坑之后,我们慢慢整理了一套定时任务的落地规则,不追求复杂调度平台,优先规避基础风险:
- 多实例部署的定时任务,必须做分布式互斥。锁超时时间大于任务最大预估耗时,同时记录任务执行状态,异常崩溃之后支持人工解锁。区分幂等任务和非幂等任务,非幂等任务对重复执行的防护等级要更高。
- 明确错过调度的策略:对账、统计这类强一致性任务开启补发;缓存刷新、简单清理这类弱一致性任务,直接丢弃错过的调度,避免大量补发任务扎堆压垮系统。
- 开启任务串行控制,禁止无限制并发堆积。配置最大运行时长,超过阈值直接告警甚至主动终止任务,防止僵死任务一直占用资源。
- 调度配置里显式指定时区,不依赖服务器默认时区,cron 配置和文档里统一标注时区。
- 依赖关系不再依靠错开调度时间。有先后依赖的任务,不再用 cron 硬排时间,改成事件驱动,A 执行成功之后主动触发 B;实在要用定时串联,增加前置状态校验,B 启动先检查 A 是否完成。
- 完善监控告警:除了失败告警,新增超时告警、执行耗时趋势告警、处理数据量异常告警。每次任务执行落地一条执行记录,方便追溯哪一期任务跑了、处理多少数据、有没有异常。
- 重任务分批、分页,避免一次性扫描全表。凌晨的定时任务同样加入资源保护,设置读写限流,防止拖垮存储。
定时任务看起来简单,只是一段到点运行的代码。但它和在线接口不一样,接口出问题马上能收到用户反馈,定时任务大多在低峰期跑,问题可以潜伏很久,等到对账的时候才集中爆发,修复和数据回补成本很高。
很多时候定时任务故障,不是业务逻辑写错了,而是我们默认它 "到点就能正常跑一次",忽略了分布式环境下的并发、时序、故障兜底。