跳到主要内容
实用工具2026-07-137 分钟阅读

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-MAR1,6 / 1-3
星期SUN / MON-FRI0 / 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 号且必须也是周五",有两种方案:

  1. 只用一个字段,另一个在脚本里判断:
0 0 1 * *

然后在脚本中:

#!/bin/bash
# 仅当今天是周五时才继续执行
if [ "$(date +%u)" -ne 5 ]; then
  exit 0
fi
# ... 实际任务逻辑
  1. 使用支持 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 解析器 做三步核对:

  1. 字段展开:看分钟/小时/日/月/周各自落到哪些数字,确认没有手滑写成 */50 之类。
  2. 可读摘要:英文短语拼装(Month / Day / Weekday / Hour / Minute),扫一眼取值集合。
  3. 接下来 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 整串右移,表现成「怎么都不触发」或「每秒狂跑」。