Cron 表达式详解:从 '* * * * *' 到复杂调度
Cron 表达式看起来很吓人——五个由星号、斜杠和数字组成的字段,却能编码出"每周二凌晨 3:30 执行"这样的含义。但只要理解了它的结构,读写 cron 调度就变得很简单。
本指南从基础讲起,拆解 cron 表达式语法,覆盖生产环境真正会用到的模式,并重点讲一个几乎所有人都踩过的坑。你可以一边读一边用我们的免费 Cron 解析器实时验证表达式。
五个字段
标准 cron 表达式有 5 个以空格分隔的字段:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12 or JAN-DEC)
│ │ │ │ ┌───────────── day of week (0-7 or SUN-SAT)
│ │ │ │ │
* * * * *
每个字段可以接受:
| 语法 | 含义 | 示例 |
|---|---|---|
* | 每个值 | * * * * * = 每分钟 |
5 | 具体值 | 5 * * * * = 第 5 分钟 |
1,15,30 | 列表 | 0 9,17 * * * = 上午 9 点和下午 5 点 |
1-5 | 范围 | 0 9 * * 1-5 = 周一到周五 |
*/5 | 步长 | */5 * * * * = 每 5 分钟 |
10-30/5 | 范围 + 步长 | 10-30/5 * * * * = 第 10、15、20、25、30 分钟 |
注意:day of week 用 0-7,其中 0 和 7 都表示周日。也可以用三字母缩写:SUN、MON、TUE、WED、THU、FRI、SAT。月份同理可用 JAN–DEC。本站 Cron 解析器 会把这些别名展开成数字,并把 7 规范为周日 0。
名称别名:写给人读,机器照样认
很多人从文档里复制 0 9 * * MON-FRI,贴进严格只认数字的校验器就失败。Vixie crontab 与多数 Linux 发行版都接受三字母英文名(大小写不敏感):
| 字段 | 别名示例 | 等价数字 |
|---|---|---|
| 月份 | JAN,JUN / JAN-MAR | 1,6 / 1-3 |
| 星期 | SUN / MON-FRI | 0 / 1-5 |
别名只是写法糖,语义与数字完全相同。团队里若有人用数字、有人用名称,用解析器展开后对照取值列表,比口头争论「周一到底是 0 还是 1」更快。
实战中常用的模式
下面这些 cron 示例能覆盖 90% 的真实需求:
每 5 分钟:
*/5 * * * *
步长 */5 表示"从 0 开始,每 5 个值一次"。触发时间是 :00、:05、:10、:15,依此类推。
每个工作日上午 9:00:
0 9 * * 1-5
第 0 分钟、9 点、任意 day of month、任意月份、周一到周五。
每月 1 号零点:
0 0 1 * *
第 0 分钟、0 点、1 号、任意月份、任意 day of week。
每周日凌晨 2:30:
30 2 * * 0
工作时段(上午 9 点到下午 5 点)每 15 分钟,仅工作日:
*/15 9-17 * * 1-5
这会在周一到周五的 9 点到 17 点之间,每小时的 :00、:15、:30、:45 触发。注意它也会在 17:15、17:30、17:45 触发——如果你想精确停在 17:00,需要两个表达式,或者用更精细的小时范围如 9-16 再加上 0 17 * * 1-5。
每小时的第 30 分钟:
30 * * * *
每天两次(上午 6 点和下午 6 点):
0 6,18 * * *
每周一、三、五上午 8 点:
0 8 * * 1,3,5
day-of-month 与 day-of-week 的那个坑
这是在生产环境咬人一口的经典问题。当你同时指定了 day-of-month 和 day-of-week 两个字段(即两者都不是 *)时,cron 使用的是 OR 逻辑,而不是 AND。
0 0 1 * 5
你可能会读成"每月 1 号零点执行,但前提是这一天得是周五"。这是错的。它的真实含义是:"每月 1 号零点执行 或者 任意周五零点执行。"
原因是原始的 Vixie cron 实现把受限的 day-of-month 和 day-of-week 当作独立的触发条件。只要满足其一,任务就会运行。
如何实现 AND 行为:
如果你确实需要"每月 1 号且必须也是周五",有两种方案:
- 只用一个字段,另一个在脚本里判断:
0 0 1 * *
然后在脚本中:
#!/bin/bash
# 仅当今天是周五时才继续执行
if [ "$(date +%u)" -ne 5 ]; then
exit 0
fi
# ... 实际任务逻辑
- 使用支持 AND 逻辑的 cron 实现。 一些现代调度器(如 Java 的 Quartz)对此处理不同。请查阅你所使用平台的文档。
安全规则: 需要 day-of-week 过滤时,把 day-of-month 设为 *;需要 day-of-month 过滤时,把 day-of-week 设为 *。除非你清楚理解 OR 行为,否则不要同时限制两者。
步长、范围和列表的深入用法
步长(/) 定义匹配之间的间隔:
*/10 * * * * → 第 0、10、20、30、40、50 分钟
0 */6 * * * → 第 0、6、12、18 小时
0 0 */2 * * → 每月的第 1、3、5……天
一个常见误解:分钟字段的 */5 并不意味着"从任务创建那一刻起每 5 分钟一次"。它的含义是"在第 5 的倍数的分钟数触发"。如果你在 10:03 创建任务,下次执行是 10:05,而不是 10:08。
范围(-) 定义一个连续区间:
0 9-17 * * * → 上午 9 点到下午 5 点每小时(含两端)
0 0 * * 1-5 → 零点,周一到周五
0 0 1-15 * * → 每月前 15 天
列表(,) 组合离散值:
0 8,12,18 * * * → 上午 8 点、中午 12 点、下午 6 点
0 0 1,15 * * → 每月 1 号和 15 号
三种可以组合使用:
0,30 9-17/2 * * 1-5
含义是:第 0 和 30 分钟,9、11、13、15、17 点,周一到周五。
真实场景示例:CI/CD 和备份
CI/CD:开发时段每 15 分钟跑一次测试:
*/15 8-20 * * 1-5
工作日早 8 点到晚 8 点每 15 分钟触发。CI 流水线会拉取新提交并跑测试套件,又不会在凌晨白白烧算力。
数据库备份:每天凌晨 2 点:
0 2 * * *
简单可靠。配合一个保留策略脚本:
#!/bin/bash
pg_dump mydb | gzip > /backups/mydb_$(date +%Y%m%d).sql.gz
find /backups -name "*.sql.gz" -mtime +30 -delete
每周备份:周日凌晨 3 点:
0 3 * * 0
月度报表:每月 1 号上午 6 点:
0 6 1 * *
证书续期检查:每天凌晨 4 点(针对 Let's Encrypt 的 90 天证书):
0 4 * * *
配套一个只在剩余 30 天内才真正续期的脚本:
#!/bin/bash
certbot renew --quiet --deploy-hook "systemctl reload nginx"
日志轮转:每周日凌晨 1 点:
0 1 * * 0
健康检查 ping:每分钟(慎用):
* * * * *
每分钟执行对于轻量健康检查没问题,但不要在这种频率上跑重活。如果你的任务执行时间超过 60 秒,加个锁文件防止运行重叠:
#!/bin/bash
flock -n /tmp/myjob.lock -c 'actual_command_here'
验证你的 cron 表达式
cron 语法错误是静默的——没有编译器帮你抓。像 0 9 * * 8(无效的 day-of-week)这种笔误,有的 cron 守护进程会拒绝,有的会静默忽略。
在把调度部署到生产之前,用 cron 解析器 做三步核对:
- 字段展开:看分钟/小时/日/月/周各自落到哪些数字,确认没有手滑写成
*/50之类。 - 可读摘要:英文短语拼装(Month / Day / Weekday / Hour / Minute),扫一眼取值集合。
- 接下来 5 次触发:按浏览器本地时区推算墙钟时间,并遵循 Vixie 的日/周 OR 语义。例如
0 0 1 * 5会同时列出「每月 1 号零点」和「每个周五零点」——若你本意是 AND,这里会当场露馅。
时区提醒:解析器的「下次触发」用的是你打开页面时的本地时钟,不是目标服务器的 TZ。云主机常为 UTC;容器镜像未设 TZ 时也常是 UTC。把本地推算结果换算到服务器时区,或直接在服务器上跑一条带 date >> /tmp/cron-smoke.log 的低频探测任务,两边对齐后再上线。
如果你要从需求反向构建调度(「我希望这东西每 4 小时在工作日执行一次」),cron 生成器 可以帮你点选拼出表达式,并同步显示下次触发预览——再贴回解析器复核。
两个工具都在浏览器本地运行,不上传表达式。备份窗口、部署节奏这类调度往往暗示内部基础设施节奏,本地计算更合适。
五字段之外:宏、秒字段与方言
标准 crontab 还有 @hourly、@daily、@weekly、@monthly、@yearly、@reboot 等宏,语义固定,不走五字段解析。Kubernetes CronJob、多数 SaaS 调度器只认五字段,不认宏——把 @daily 贴进 CronJob YAML 会直接失败。
反向的坑是 多一段:Quartz / Spring @Scheduled 默认带秒字段(六段或七段)。把 Linux 的 0 2 * * * 原样贴进 Quartz,整串右移一位,表现成「怎么都不触发」或「每秒狂跑」。迁移前先数段数:本站解析器/生成器绑定经典五字段,六段会报 INVALID_CRON_FORMAT。
速查表
| 调度 | 表达式 |
|---|---|
| 每分钟 | * * * * * |
| 每 5 分钟 | */5 * * * * |
| 每小时 | 0 * * * * |
| 每天零点 | 0 0 * * * |
| 工作日上午 9 点 | 0 9 * * 1-5 |
| 每周日凌晨 2 点 | 0 2 * * 0 |
| 每月 1 号中午 | 0 12 1 * * |
| 每 6 小时 | 0 */6 * * * |
| 每天两次(上午 8 点、下午 8 点) | 0 8,20 * * * |
| 工作日 9-17 点每 15 分钟 | */15 9-17 * * 1-5 |
把这一页加到书签,或者更好的做法是:写调度时永远开着一个 cron 解析器 标签页。花五分钟验证一个表达式,能省下凌晨三点被叫起来处理备份没跑的麻烦。
排查注记
全文围绕一个坑展开——day-of-month 与 day-of-week 同时受限时 cron 是 OR 不是 AND,开篇就预告「几乎所有人都踩过」;安全规则因此写得武断:需要周过滤就把日设为 *,除非清楚理解 OR 行为,不要同时限制两者。验证一节的理由是「cron 语法错误是静默的,没有编译器帮你抓」,三步核对里「接下来 5 次触发」故意按 OR 语义推算——0 0 1 * 5 会同时列出每月 1 号和每个周五,本意是 AND 的话当场露馅。方言一节收的六段坑同样典型:五字段原样贴进 Quartz 整串右移,表现成「怎么都不触发」或「每秒狂跑」。
相关工具
相关文章
Markdown 转 Word 交付指南:样式映射与行内降级
把 Markdown 草稿变成可交付的 .docx:pandoc 命令行与浏览器本地转换两条路线,标题/表格/代码块的 Word 原生样式映射对照,行内格式降级清单,以及「可编辑」为什么比「看起来像」更重要。
Word 转 Markdown 怎么转才干净:两条路线与格式保真对照
docx 转 Markdown 教程:pandoc 命令行与浏览器本地转换两条路线对比,标题/表格/图片/自动编号的保真行为,.doc 旧格式、图片体积、合并单元格等常见坑的解法。
中国车牌号规则与校验:普通牌、新能源与特殊格式全解
中国大陆车牌号编码规则详解:普通蓝牌、新能源 GA36D-2018、武警与使馆牌的结构、省份简称与发牌机关编码,附在线校验方法。