Cron 表达式从入门到精通:5 个字段背后的逻辑与踩坑
0 0 * * * 和 * * * * 0,哪个是"每天凌晨",哪个是"每分钟,仅周日"?
Cron 是后端、运维、数据工程师的高频工具,但偏偏语法极易写错——一个字段顺序记反,备份就可能从"每天"变成"每月"。更尴尬的是不同环境(Linux crontab、Quartz、Kubernetes CronJob、AWS EventBridge)的语法略有差异,跨平台抄一份配置容易翻车。
这篇文章从 5 字段语法讲起,覆盖各种方言、常用范式、调试方法和踩坑案例。看完不再写错。
5 个字段:Cron 表达式的核心结构
标准 Linux Cron 表达式由空格分隔的 5 个字段组成:
记忆口诀:分 时 日 月 周 —— 从小到大,再到周。
| 字段 | 取值范围 | 特殊值 |
|---|---|---|
| 分钟 | 0-59 | — |
| 小时 | 0-23(24 小时制) | — |
| 日(月内第几天) | 1-31 | L = 月末(部分实现) |
| 月 | 1-12 | JAN-DEC 也行 |
| 周几 | 0-7(0/7 都是周日) | SUN-SAT 也行 |
周日是 0 还是 7? 标准 Linux Cron 两个都接受。但在 Quartz/Java 体系(Spring Schedule)周日是 1,字段顺序也不同——见后文方言对比。
5 个特殊字符:把死板的字段变灵活
掌握下面 5 个字符,能表达 99% 的调度需求。
1. 星号(*)— 任意值
2. 逗号(,)— 列举多个值
3. 连字符(-)— 范围
4. 斜杠(/)— 步长(间隔)
5. 问号(?)— 不指定(仅 Quartz 等扩展方言)
标准 Linux Cron 不支持 ?,但 Quartz / Spring / AWS 等用它来表达"该字段不关心",常用于消解"日"和"周几"的冲突(见下文)。
必须搞清楚的坑:日和周几的逻辑关系
新手 100% 会踩的坑:
你以为是「每月 1 号且必须是周日才跑」?错。
Linux Cron 里,日和周几是 OR 关系——不是 AND,只要满足任意一个就执行。所以上面这条相当于:
每个月 1 号 0 点跑一次 加上 每个周日 0 点跑一次
如果真想要"周日且月初"的"与"关系,必须借助 shell:
或者改用支持 ? 的 Quartz 体系,逻辑会更清晰。
为什么 Cron 这么设计? 因为 Vixie Cron 的设计哲学是「宽松匹配」——给定多个限制条件时,宁可多跑也不少跑。这逻辑反直觉,是历史遗留。
常用范式速查表
把这张表收藏,写 Cron 直接抄。
| 需求 | 表达式 | 备注 |
|---|---|---|
| 每分钟 | * * * * * | 慎用,可能撞资源 |
| 每 5 分钟 | */5 * * * * | — |
| 每小时整点 | 0 * * * * | — |
| 每 2 小时 | 0 */2 * * * | 0,2,4,...,22 点 |
| 每天凌晨 0 点 | 0 0 * * * | — |
| 每天 8:30 | 30 8 * * * | — |
| 每天 8:00 和 20:00 | 0 8,20 * * * | — |
| 工作日 9-18 点整点 | 0 9-18 * * 1-5 | — |
| 每周一早 7 点 | 0 7 * * 1 | 1=周一 |
| 每周日晚 22 点 | 0 22 * * 0 | 0=周日 |
| 每月 1 号 0 点 | 0 0 1 * * | — |
| 每月最后一天 | 0 0 28-31 * * + shell 判断 | Linux 原生无 L |
| 每季度第一天 | 0 0 1 1,4,7,10 * | — |
| 每年元旦 | 0 0 1 1 * | — |
| 每 15 秒 | (Linux Cron 不支持) | 需用 systemd timer 或 Quartz |
Linux 还支持的快捷别名
方言对比:跨平台抄配置前先看一眼
| 体系 | 字段数 | 顺序 | 周几起点 | 支持秒 | 支持 ? L W |
|---|---|---|---|---|---|
| Linux Vixie Cron | 5 | 分 时 日 月 周 | 0=周日 | ❌ | ❌ |
| Quartz / Spring Schedule | 6 或 7 | 秒 分 时 日 月 周 [年] | 1=周日 | ✅ | ✅ |
| Kubernetes CronJob | 5 | 分 时 日 月 周 | 0=周日 | ❌ | ❌ |
| AWS EventBridge | 6 | 分 时 日 月 周 年 | 1=周日 | ❌ | ✅ |
| GitHub Actions | 5 | 分 时 日 月 周 | 0=周日 | ❌ | ❌ |
| Jenkins | 5 | 分 时 日 月 周 | 0=周日 | ❌ | 仅 H(哈希) |
| node-cron / node-schedule | 5 或 6 | [秒] 分 时 日 月 周 | 0=周日 | ✅ | 部分 |
三个最常踩的方言坑
坑 1:Quartz 比 Linux 多一个秒字段
Spring @Scheduled(cron = "0 0 12 * * ?") 在 Linux 里写就是 0 12 * * *——后者写五个,前者写六个。
坑 2:Quartz 周日是 1,Linux 周日是 0
Linux "每周一 0 点" 是 0 0 * * 1,Quartz 对应是 0 0 0 ? * 2(第六位 2 表示周一;日字段必须用 ?)。
坑 3:Quartz 日/周不能同时指定
Quartz 要求日和周二选一,另一个必须用 ?。0 0 12 1 * 1 在 Quartz 里非法,必须写 0 0 12 1 * ? 或 0 0 12 ? * 1。
实战范例:5 个真实业务场景
场景 1:每天凌晨 3 点做数据库备份
关键点:
- 选凌晨而非整点,错开高峰
- 必须重定向日志,否则 cron 出错没人知道
2>&1把 stderr 也收进同一文件
场景 2:工作日早上 9 点发日报
注意:节假日还是会跑——如要排除,脚本内自己判断。
场景 3:每 10 分钟检查一次服务健康
-fsS 让 curl 在 HTTP 错误码时返回非 0,触发后面的 ||。
场景 4:每月第一个周一 0 点做月度统计
Linux Cron 没法直接表达"第一个周一"——前面讲过日和周是 OR 关系,不能直接用 1-7 * 1(那会触发每月 1-7 号每天 + 每周一的并集)。必须靠 shell 守卫:
1-7:日字段限定在月初 7 天- 周字段留
*(关键,否则 OR 起来反而执行更多次) - shell 里
date +%u返回 ISO 周几(1=周一),等于 1 才执行
每月只有一天同时满足"日在 1-7"和"周一",正好等于第一个周一。
场景 5:每年税务统计(只跑一次)
避开整点高峰:很多脚本写在 0 0 * * *,全公司服务器同一秒触发会引发雪崩。建议错开到 5 0 * * *、13 0 * * * 这种带"偏移分钟"的时刻。
5 个最常见的踩坑
1. Cron 不识别你的环境变量
Cron 用的是极简环境,连 $PATH 都精简到只有 /usr/bin:/bin。脚本里如果用了 python、node、docker,得写全路径或在脚本顶部 export PATH=...。
2. 百分号必须转义
Crontab 里 % 是特殊字符,会被替换成换行。脚本里用 date +%Y-%m-%d 这种,在 crontab 里要写成:
每个 % 前加反斜杠。
3. 时区问题
Linux Cron 用系统时区。容器里跑 cron 经常默认 UTC,"每天 8 点"实际上是北京时间 16 点。
CRON_TZ 必须写在 crontab 文件最前面,且仅部分 Cron 实现支持。
4. 任务并发堆积
如果脚本本身要跑 20 分钟,但你设了 */10 * * * *,第二轮会在第一轮没结束时启动。用 flock 互斥:
-n 表示拿不到锁就直接退出,避免堆积。
5. 步长起点的差异(2/30 vs */30)
*/30 * * * *→ 0 分、30 分(从 0 开始每 30)2/30 * * * *→ 2 分、32 分(从 2 开始每 30)
很多人误以为 */30 是 30, 60,0 才是起点。
调试技巧
写完先用工具验证
不要直接上生产。本站的 Cron 解析器 会告诉你这条表达式的下次执行时间——眼睛看不出错,工具看得出来。
让 cron 真的跑起来再说
用 MAILTO 把错误寄到邮箱
脚本有 stderr 输出时,Cron 会把错误邮件发给指定邮箱。前提是系统装了 mail 命令并配好 MTA。
替代方案:什么时候不要用 Cron
Cron 几十年没怎么进化,下面这些场景别硬上:
| 需求 | 更好的方案 |
|---|---|
| 需要秒级精度或更复杂调度 | systemd timer / Quartz |
| 需要执行历史、重跑、可视化 | Airflow / Dagster / Prefect |
| 需要分布式调度、任务依赖 | xxl-job / Argo Workflows |
| K8s 集群里的定时任务 | Kubernetes CronJob |
| 函数计算 / Serverless | AWS EventBridge / 阿里云函数定时触发 |
| 一次性延迟任务 | at 命令 / 消息队列延迟消息 |
总结
Cron 看似简单,5 个字段背后有不少历史包袱:
- 字段顺序:分时日月周,从小到大再到周
- 日和周是 OR 不是 AND——这是 Linux Cron 最大的反直觉点
- 跨平台先看方言:Quartz 多一个秒字段、周日起点不同
- 写完用工具验证:肉眼看 Cron 表达式很容易看错
- 环境/时区/转义:脚本能跑不代表 Cron 能跑
把这篇文章和站内的 Cron 解析工具 配合用——写表达式时输入工具看下次触发时间,几乎能消灭 90% 的 Cron 配置错误。