发布于 · 阅读约 3 分钟
从零开始学 cron 表达式:可视化教程
学会 POSIX 五字段 cron 语法、特殊字符、步长值、Quartz 扩展、AWS cron 的差异,以及十几个能直接粘进 crontab 的实战示例。
为什么 cron 至今无处不在
cron 是 Brian Kernighan 于 1979 年为 Version 7 Unix 写的程序。现代 Linux 系统仍在使用的版本——Paul Vixie 1987 年写的 Vixie cron——增加了环境变量、按用户的 crontab 和友好的 @reboot 简写,但几乎原封不动地保留了最初的五字段调度格式。正是这种稳定性,让一种诞生于万维网之前的语法,在 2026 年依然出现在 Kubernetes 清单、GitHub Actions 工作流、AWS EventBridge 规则和 Spring @Scheduled 注解里。
如果你学 cron 只靠从 Stack Overflow 复制行,迟早会出意外——每日备份变成每分钟跑一次,周末任务在周二触发,DST 切换时时间偏移。本文从基础讲起,再给出足够多的实战示例,让你一眼就能认出任何表达式的形状。
五字段格式
POSIX 的 crontab 行由调度表达式和命令组成。调度部分由空白分隔的恰好五个字段构成:分钟、小时、日、月、周。每个字段都接受数字、列表、范围、步长或通配符 *。
* * * * * command-to-run
┬ ┬ ┬ ┬ ┬
│ │ │ │ │
│ │ │ │ └─── 周 (0-6,周日 = 0;多数实现里 7 也代表周日)
│ │ │ └───── 月 (1-12,或 JAN-DEC)
│ │ └─────── 日 (1-31)
│ └───────── 小时 (0-23)
└─────────── 分钟 (0-59)特殊字符:* , - / 和它们的伙伴
五个字符承担了经典 cron 里几乎所有的语义。学会它们各自的含义后,大多数表达式读起来就像一句话。
- * —— 该字段的全部取值。分钟字段为 * 表示「每分钟」。
- , —— 离散值列表。分钟字段写 0,15,30,45 表示每小时四个固定时刻。
- - —— 闭区间范围。周字段写 1-5 表示周一到周五。
- / —— 步长,写法为 范围/步长 或 */步长。分钟字段写 */10 表示「从 0 开始每 10 分钟」。
- L、W、# —— Quartz 扩展,不属于 POSIX。L = 最后,W = 最近的工作日,# = 当月第 n 个星期几。后文会讲。
0,30 * * * * 每小时的 :00 和 :30
*/5 * * * * 每 5 分钟(0、5、10、15...)
0 9-17 * * 1-5 周一到周五,09:00 到 17:00 每小时
0 0 1,15 * * 每月 1 日和 15 日的午夜
15 14 * * 0 每周日 14:15步长值并不是你以为的那样
步长操作符 / 永远作用在一个范围上,即便范围是隐式的。分钟字段写 */15 等价于 0-59/15,会在 0、15、30、45 触发。所以分钟里写 */7 并不表示「从现在起每 7 分钟」,而是「0 到 59 中所有 7 的倍数」,也就是 0、7、14、21、28、35、42、49、56,然后跨越午夜前会留 4 分钟空档。如果你需要真正的滚动间隔,请用任务运行器或 systemd 计时器的 OnUnitActiveSec。
步长也可以从范围中部开始。分钟字段写 5-59/10 会在 5、15、25、35、45、55 触发。当你需要非对称调度又不想写一长串逗号列表时,把范围和步长结合起来。
*/15 * * * * 00、15、30、45 (正确)
*/7 * * * * 00、07、14、21、28、35、42、49、56,然后 4 分钟空档(多半不是你想要的)
5-59/10 * * * * 05、15、25、35、45、55
0 */4 * * * 00:00、04:00、08:00、12:00、16:00、20:00日字段和周字段:那个 OR 规则
cron 最大的踩坑点:当日和周两个字段都被限制(都不是 *)时,Vixie cron 把它们当成逻辑 OR,而不是 AND。0 0 13 * 5 这一行表示每月 13 号的午夜「或」每周五——而不是只在「13 号且是周五」时触发。要做 AND,你只能限制其中一个字段、在脚本里再过滤另一个,或者用支持 ? 占位符(表示「不指定」)的调度器如 Quartz。
0 0 13 * 5 每月 13 号的午夜,或任意周五(Vixie cron 的 OR 规则)
0 0 * * 5 每周五午夜
0 0 13 * * 每月 13 号午夜
# Quartz: 0 0 0 13 * 5 ? 才会精确匹配「13 号且是周五」十个直接复制的示例
把这张表打印出来贴在显示器旁。你以后写的多数生产 cron 行,都只是这些示例的微调。
* * * * * 每分钟
0 * * * * 每小时整点
*/15 * * * * 每 15 分钟
0 0 * * * 每天午夜(服务器本地时间)
0 9 * * 1-5 工作日 09:00
30 14 * * 1-5 工作日 14:30
0 9 * * 1 每周一 09:00
0 22 * * 0 每周日 22:00
0 0 1 * * 每月 1 日午夜
0 0 1 1 * 1 月 1 日午夜
0 3 * * 6 每周六 03:00(典型备份时段)
*/5 9-17 * * 1-5 工作日营业时间内每 5 分钟命名简写
Vixie cron、GNU mcron 及其后代大多支持以 @ 开头的别名,对应常见调度。它们比五个字段更易读,也更不容易打错。
@yearly 等同于 0 0 1 1 * (也叫 @annually)
@monthly 等同于 0 0 1 * *
@weekly 等同于 0 0 * * 0
@daily 等同于 0 0 * * * (也叫 @midnight)
@hourly 等同于 0 * * * *
@reboot 系统启动时执行一次Quartz:6 或 7 字段的表兄
Java 调度器——Quartz 本身、Spring 的 @Scheduled(cron = ...)以及一些云平台——使用另一种方言。Quartz 在前面加一个秒字段,可选地在末尾加一个年字段,总共 6 或 7 个字段。它还支持 L(最后)、W(最近工作日)、#(当月第 n 个星期几)以及 ? 占位符——表示「该字段不指定,日和周中只设其一」。
如果把 Quartz 表达式复制到 Linux 的 crontab,会被静默解析错,因为开头那个 0 在 Vixie cron 看来只是普通的分钟值。保存文件前,永远先确认你的调度器用的是哪种方言。
Quartz 字段:秒 分 时 日 月 周 [年]
0 0 12 * * ? 每天中午 12 点
0 15 10 ? * MON-FRI 工作日 10:15
0 0 0 L * ? 每月最后一天午夜
0 0 0 LW * ? 每月最后一个工作日
0 0 9 ? * 2#1 每月第一个周一 09:00
0 0 9 ? * 6#3 每月第三个周五 09:00
0 0 0 1 * ? 2026 仅 2026 年内,每月 1 日午夜AWS 的 rate 与 cron 表达式
EventBridge、CloudWatch Events 和 Lambda 用的是接近 Quartz 的 cron 语法,但有两个重要差别:没有秒字段(共 6 个字段,不是 7 个),并且日和周字段必须有且只有一个为 ?,不能都为 *。AWS 同时提供 rate(value unit) 用于简单的间隔触发,可彻底回避 OR 陷阱。
AWS cron 字段:分 时 日 月 周 年
cron(0 12 * * ? *) 每天 12:00 UTC
cron(0/15 * * * ? *) 每 15 分钟
cron(0 9 ? * MON-FRI *) 工作日 09:00 UTC
cron(0 0 1 * ? *) 每月 1 日 UTC 午夜
rate(5 minutes) 每 5 分钟
rate(1 hour) 每小时
rate(7 days) 从规则创建起每 7 天时区、夏令时和那些在 02:30 出问题的东西
Vixie cron 遵循系统时区(TZ 环境变量,或 /etc/localtime 所指的时区)。夏令时跳一小时向前时,落在那段空窗里的任务会被静默跳过;夏令时回拨时,被复制的那一小时内的任务在某些实现里会跑两次,另一些则只跑一次。两个安全的默认做法:让 cron 跑在 UTC,或者把任务安排在 01:00–04:00 区间之外。
云端调度器行为各异:EventBridge 永远按 UTC 跑;Kubernetes CronJob 自 v1.27 起支持 spec.timeZone 字段;Quartz 默认按 JVM 默认时区,除非你给 CronTrigger 显式传时区。每用一个新的调度器前都先看一眼文档,别假定上一个的约定还成立。
调试不触发的表达式
十之八九,表达式本身没错,是环境出了问题。在你怀疑调度之前,先走一遍这个清单。
- 读 cron 日志:Debian/Ubuntu 上 grep CRON /var/log/syslog,systemd 主机上 journalctl -u cron。日志里有 cron 实际执行的命令行。
- 确认是哪个用户。不带参数的 crontab -l 显示当前用户的 crontab;你关心的调度可能在 /etc/crontab、/etc/cron.d/* 或别的用户的 crontab 里。
- 检查 PATH。cron 运行时 PATH 极简(通常是 /usr/bin:/bin)。要么用绝对路径,要么在 crontab 顶部设置 PATH=。
- 检查 shell。cron 用 /bin/sh,不是你的登录 shell。任何 bash 专有的语法(进程替换、[[ ]])都需要显式用 bash -c 包一层。
- 重定向输出。没有 > /var/log/myjob.log 2>&1,失败的任务会把邮件发给本地 root,你永远看不到。
- 确认时区。在任务里同时执行 date && date -u。如果两者与你假设的时间不一致,那就是你的 bug。
# 一行更稳妥的 cron:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
*/5 * * * * /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1什么时候 cron 不是合适的工具
cron 擅长固定的挂钟时间调度,对单机上的一行命令几乎无敌。但它不适合:跨多机的分布式协调(用队列、领导选举调度器或带 concurrencyPolicy: Forbid 的 Kubernetes CronJob);失败时的重试与退避(写脚本包一层或用工作流引擎);亚分钟粒度(用 systemd 计时器或事件驱动触发);以及依赖上一次跑完才能开始下一次的调度(cron 会在前一次还没结束时就再起一份)。早点意识到这些边界,能省掉很多凌晨 3 点的告警。
可视化构建与验证
如果你一个月写超过几行 cron,最有杠杆的习惯就是在提交前把每个表达式扔进解析器对一下。Multilities 提供了免费的 /tools/cron-builder,可把 5 或 6 字段表达式翻译成自然语言,并按本地时区显示接下来 10 次触发时间——这正是能抓住「日/周 OR 陷阱」和「*/7 步长意外」的交叉验证。再配上一个针对已知锚定日期的下次触发时间断言的单元测试,你的定时任务就能稳到几乎无聊——这才是你想要的状态。