定时任务的两个阶段
系统的定时任务会经历两个阶段:
- 开发期:任务是自己写的,跑不跑得对由开发保证;
- 交付后:客户要加"每天早上 8 点给未付款订单发提醒",如果没有可配置的任务模块,就只能改代码。
所以定时任务必须做成模块,而不是散落在脚本里。
任务表:把"什么时间做什么"变成数据
两条表分工明确:
- 任务表:任务名、执行标识(handler)、cron 表达式、参数、状态、下次执行时间、最后执行结果;
- 日志表:每次执行的开始时间、结束时间、耗时、是否成功、输出摘要、失败原因。
关键设计是把执行逻辑做成注册表:代码里注册一批可执行单元(关单、逾期扫描、静态页生成、数据备份),任务表里只存标识与参数。这样新增任务不需要改调度器,只需要注册一个新单元。
这样做还有一个额外好处:任务表可以随租户/环境不同而不同,一套代码在 A 客户那里跑 5 个任务,在 B 客户那里跑 8 个,互不影响。
cron 解析:自己写还是用库
我们选了自己实现一个最小解析器,原因有三:
- 需要支持的标准子集很小(分、时、日、月、周五个字段,支持
*、列表、区间、步长); - 引入第三方库意味着多一个依赖,而私有化部署环境不允许随意
composer install; - 解析结果要能算出"下次执行时间",用于展示与去重,这部分逻辑必须自己掌握。
解析的实现要点是:把当前时间向下取整到分钟,逐分钟向前推进,匹配成功即返回。这种"笨办法"在分钟级调度上完全够用,且不会因为月份边界、闰年、跨年出现漏算。
执行器:三个必须解决的问题
第一,重复执行。 多进程、多实例同时扫描到同一个到期任务,会重复跑。解决方式是执行前用条件更新抢占:只有把"下次执行时间"成功推进的任务,才拿到执行权。
第二,单点失败不拖累全局。 一个任务抛异常,不能让本轮其它任务全部跳过。执行器对每个任务单独 try/catch,失败写日志、置失败状态,继续下一个。
第三,执行时长。 任务耗时过长会拖住整轮调度。做法是记录耗时,并在前端展示,超过阈值时告警,必要时把任务拆成小批次(例如静态页生成按 50 个目标一片)。
前端:让运营看得懂
任务管理页提供:列表(状态、cron、下次执行、最后结果)、启停、立即执行一次、查看执行日志、手动改 cron。
「立即执行一次」是最常用的功能:客户说"今天的日报没出来",运营点一下就能验证是任务配置问题还是数据问题。
一条经验
定时任务最容易出的不是"跑不对",而是"以为在跑其实没跑":
- cron 写错一位,永远不触发;
- 上次执行失败后状态被置为停用,没人发现;
- 时区不对,任务在凌晨 4 点跑而不是 8 点。
对策只有一个:每次执行都留日志,并在管理页显式展示"下次执行时间"。让人一眼看到"它到底会不会跑"。