Skip to content
utility2026-07-256 分钟阅读

Cron 表达式看起来很吓人——五个由星号、斜杠和数字组成的字段,却能编码出"每周二凌晨 3:30 执行"这样的含义。但只要理解了它的结构,读写 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 都表示周日。也可以用三字母缩写:SUNMONTUEWEDTHUFRISAT

实战中常用的模式

下面这些 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 解析器 验证一下,它会以人类可读格式展示接下来的几次执行时间。看到"下次执行:7 月 28 日周一 9:00、7 月 29 日周二 9:00……",就能立刻判断表达式是否符合预期。

如果你要从需求反向构建调度("我希望这东西每 4 小时在工作日执行一次"),cron 生成器 可以帮你生成表达式——然后再用解析器验证一下逻辑是否正确。

这两个工具都完全在浏览器中运行,不会发送任何数据。这对于那些会暴露内部基础设施时间安排(备份窗口、部署节奏、维护时段)的调度来说尤为重要。

速查表

| 调度 | 表达式 | |----------|-----------| | 每分钟 | * * * * * | | 每 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 解析器 标签页。花五分钟验证一个表达式,能省下凌晨三点被叫起来处理备份没跑的麻烦。


ad