Cron 是紧凑的调度语法,不是完整的任务管理系统。可靠的定时任务还需要明确实现方言、时区、错过执行时的行为、并发控制、可观测性、重试和幂等性。本文把表达式与调度器分成两个独立契约。

核心要点

  • Unix 风格五字段、Quartz 六/七字段和 JavaScript 库的语法不能直接互换。
  • ?LW# 是方言相关语法,必须用实际调度器验证。
  • 日历调度依赖时区,可能遇到夏令时跳过、重复或错过本地时间。
  • Cron 不自动提供分布式锁、重试、去重或 exactly-once 执行。
  • 私有调度配置优先使用本地且锁定版本的校验器,不要把内部信息粘贴到在线服务。

先确认方言

实现家族 常见字段数 关键差异
Unix 风格 crontab 5 分钟、小时、日期、月份、星期;特殊语法依赖守护进程
Quartz/Spring 6 或 7 秒字段在最前,通常支持 ?LW# 和可选年份
应用语言库 5 或 6 语法和时区支持取决于具体包与版本

一个以 0 开头的六字段表达式可能是 Quartz 的“整分执行”,但不是合法的五字段 crontab。代码审查和文档中应同时记录方言与时区。

Unix 风格五字段语法

text
分钟 小时 日期 月份 星期
 0    9    *    *    1-5

常见运算符是 *(所有允许值)、,(列表)、-(范围)和 /(步进)。月份、星期名称以及周日使用 0 还是 7,都要以具体实现文档为准。

典型五字段示例:

表达式 目标调度
*/5 * * * * 每 5 分钟
0 9 * * 1-5 工作日 09:00
0 0 1 * * 每月 1 日午夜
30 4 1,15 * * 每月 1 日和 15 日 04:30

在许多 Vixie-cron 衍生实现中,当日期和星期都被限制时,满足其中任意字段就可能执行,而不是必须同时满足。安排窄范围日历任务前必须核对守护进程文档。

Quartz 与语言库方言

Quartz 常见格式为:

text
秒 分钟 小时 日期 月份 星期 [年份]
0  0    9    ?    *    MON-FRI

这里的 ? 表示日期或星期字段“不指定具体值”。LW# 可能被 Quartz 接受,却会被 Unix 守护进程或某些库拒绝。node-cron、APScheduler、Spring、Kubernetes 控制器和云调度器各有解析器与时区规则,必须以实际版本的文档和测试为准。

代码示例

带受限环境的 Crontab

cron
SHELL=/bin/sh
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=

# 使用绝对路径,并让任务具备幂等性
0 2 * * * /opt/jobs/backup --date=today >>/var/log/backup.log 2>&1

不要假设交互 Shell 的 PATH、工作目录、Locale、凭据或环境变量。使用专用服务账号、严格文件权限和日志轮转策略。自动化中不要使用 crontab -r,它会删除当前用户的整个 crontab。

Node.js(node-cron,请核对安装版本)

javascript
const cron = require('node-cron');

const expression = '*/5 * * * *';
if (!cron.validate(expression)) {
  throw new Error('invalid node-cron expression');
}

let running = false;
cron.schedule(expression, async () => {
  if (running) return; // 多实例应使用分布式锁或队列去重。
  running = true;
  try {
    await runIdempotentJob();
  } finally {
    running = false;
  }
});

进程内标志只能保护单个进程。多副本部署需要数据库租约、队列唯一键或调度器并发策略。

Python 与显式时区

python
from datetime import datetime
from zoneinfo import ZoneInfo
from croniter import croniter

zone = ZoneInfo("America/New_York")
base = datetime.now(zone)
schedule = croniter("0 9 * * 1-5", base)
next_run = schedule.get_next(datetime)
print(next_run.isoformat())

croniter 解析五字段表达式,但不会把任务变成分布式 Worker。使用带时区的 datetime,锁定库版本,并对夏令时切换编写测试。

Spring/Quartz 风格 Java

java
@Scheduled(
    cron = "0 0 9 * * MON-FRI",
    zone = "America/New_York"
)
public void runDailyReport() {
    reportService.runIdempotently();
}

上面的六字段表达式包含秒。Spring 与 Quartz 的解析器相关但不完全相同,应针对项目实际依赖编译并测试。

时区与夏令时

“09:00”没有时区就不是完整的调度规则。先决定它表示业务时区的民用时间,还是 UTC 时间点。春季跳转时某些本地时间不存在;秋季回拨时某些本地时间会出现两次。不同实现可能跳过、延迟或执行两次,或者只读取主机时区。

至少记录:

  • IANA 时区标识,而不是只有缩写;
  • 不存在和重复本地时间的行为;
  • 服务停机或错过执行后的行为;
  • 由主机、容器、库还是控制平面解释时区。

不要假设 CRON_TZ 到处可用;先确认守护进程是否支持,以及它影响后续哪些条目。

表达式之外的可靠性

Cron 只负责发起一次尝试,不保证任务成功:

  1. 用业务键或运行 ID 实现幂等;
  2. 设置超时并记录开始、结束、结果和调度器身份;
  3. 用锁、队列唯一键或调度器策略阻止重叠;
  4. 在表达式之外定义重试与退避;
  5. 明确错过运行时是跳过、补跑一次还是逐个补跑;
  6. 同时告警失败和“完全没有运行”。

多主机系统若需要主节点选举、持久重试、并发限制、审计历史或依赖图,应考虑托管调度器或队列。

校验与测试

用固定 fixture 测试解析器和下一次运行时间:

text
dialect: "five-field-crontab"
zone: "Europe/Berlin"
expression: "0 9 * * 1-5"
assert:
  - "下一次运行是工作日的民用时间"
  - "覆盖夏令时跳过和重复"
  - "任务键阻止重复处理"

在 CI 中使用与生产相同的库或守护进程版本校验语法。测试月末、闰日、星期日约定、夏令时、进程重启、锁过期、超时、重试和畸形配置。

常见问题

为什么表达式在一个工具里有效,在另一个工具里无效?

因为 Cron 是多个方言的集合。比较字段数、字段顺序、运算符、日期/星期语义和时区处理,再使用生产环境的同一解析器。

如何安全调度本地时间?

选择 IANA 时区,使用带时区的时间实现,记录夏令时行为,并测试不存在/重复时间。如果业务规则本质上是时间点,就存储并调度 UTC 时间点。

Cron 会防止任务重叠吗?

不会。使用锁、队列唯一性或调度器并发策略;任务本身也应在两个实例同时运行时保持安全。

Cron 能每秒执行吗?

传统 crontab 不能。支持六字段的应用调度器可能支持秒级,但要核对解析器和资源行为。高频工作通常更适合长驻 Worker 或队列。

可以使用在线 Cron 生成器吗?

只应对公开或合成表达式使用,并先核对服务政策。私有调度可能暴露主机名、内部路径、租户名或运维窗口;本地校验更稳妥。

延伸阅读

总结

Cron 表达式只是调度设计的起点,不是可靠性机制。先确认方言,明确时区和夏令时策略,用生产解析器校验,并围绕任务补齐幂等、锁、重试、观测和恢复,才能把五字段字符串变成可依赖的生产操作。