# Cron 表达式语法：常见写法、时区陷阱与验证方法

> 读懂并编写分钟、小时、日期与星期的 Cron 调度，避开日期字段的经典陷阱，并在上线前验证下一次运行时间。

**快速答案:** 标准 Cron 表达式有五个字段：分钟、小时、月内日期、月份和星期。每个字段支持数字、范围、列表、步长和通配符。从左到右阅读，注意日期和星期是“或”的关系，并在部署前确认目标时区。

## 核心要点

- 五个字段依次是分钟、小时、月内日期、月份、星期。
- */5 这类步长只作用于所在字段，不代表整个调度每隔 5 分钟。
- 经典 Cron 中日期和星期同时受限时是“或”关系，很少符合预期。
- Cron 使用服务器本地时区，夏令时切换可能跳过或重复任务。
- 上线前先验证表达式，并确认接下来几次运行时间。

---

Cron 表达式很短，却很容易读错。一个字符就能改变调度，同样的表达式在不同实现里也可能表现不同。本文讲清楚实践中真正重要的语法，以及在上线前发现错误的检查方法。

## 五个字段

标准 crontab 一行由五个时间字段和命令组成：

```text
* * * * * command
│ │ │ │ │
│ │ │ │ └── 星期（0-7，星期日是 0 或 7）
│ │ │ └──── 月份（1-12）
│ │ └────── 月内日期（1-31）
│ └──────── 小时（0-23）
└────────── 分钟（0-59）
```

字段是位置相关的。0 9 * * 1 表示每周一 09:00，而不是每月 1 日 09:00。

每个字段的范围很容易记混：分钟 0–59，小时 0–23，月内日期 1–31，月份 1–12，星期 0–7，其中 0 和 7 都表示星期日。超范围的值通常会在安装时被拒绝，但错误可能只写进系统日志，于是 crontab 看起来装好了，那一行却被悄悄忽略。

crontab 还接受五个字段之外的写法。@daily、@hourly、@weekly、@monthly 和 @reboot 被广泛支持，但它们不属于 POSIX 规范，精简的 cron 实现可能直接拒绝。以 # 开头的行是注释。命令里的 % 有特殊含义：cron 把它当作换行，后面的内容会变成标准输入，所以 date +%F 这类命令可能把自己截断。应写成 \%，或者把命令移进脚本。

## 字段语法

每个字段支持：

| 语法 | 含义 | 示例 |
| --- | --- | --- |
| * | 所有值 | 分钟字段的 * 表示每分钟 |
| 5 | 单个值 | 第 5 分钟 |
| 1-5 | 范围 | 1 点到 5 点 |
| 1,15,30 | 列表 | 第 1、15、30 分钟 |
| */15 | 从字段起点开始步进 | 每 15 分钟 |
| 10-50/10 | 在范围内步进 | 第 10、20、30、40、50 分钟 |

步长在每个单位起点重新开始。分钟字段的 */15 会在 0、15、30、45 分运行，而不是“距离上次运行 15 分钟”。

Vixie 系 crontab 还支持月份和星期的三字母缩写，例如 JAN、MON、FRI，但并非所有实现都认。范围和列表可以混用名称与数字，读起来却比纯数字更费劲。应选定一种风格并保持一致。带显式起点的步长，例如 5/15，在不同实现里含义不同：有的读作“从第 5 分钟开始，每 15 分钟”，有的直接报语法错误。起点重要时应写 5-59/15。

10-50/10 也值得仔细读：它表示 10、20、30、40、50，而不是从字段起点开始每隔十个值。如果范围不能被步长整除，末尾超出的值不会被调度，单个字段内部也没有回绕。

## 常见调度写法

- 每分钟：* * * * *
- 每 5 分钟：*/5 * * * *
- 每小时整点：0 * * * *
- 每天 03:30：30 3 * * *
- 工作日 09:00：0 9 * * 1-5
- 每月 1 日零点：0 0 1 * *
- 每周日 23:00：0 23 * * 0
- 工作时间每 15 分钟：*/15 9-17 * * 1-5

这些示例都假设服务器时区符合预期，而这个假设值得单独检查。

有些需求无法用五个字段干净表达。“每月最后一天”不是 Cron 字段，31 号在小月不会运行。“每月最后一个工作日”“第三个星期四”“每 90 分钟”都超出了五字段语法。可行的绕法是每天运行并在命令里判断日期，或换用支持日历和间隔语义的调度器。用看似聪明的范围去近似，通常会得到一条每年错一次的表达式，而那次往往没人盯着。

## 日期与星期的经典陷阱

在经典 Cron 中，如果月内日期和星期都受限，任务会在**任意一个**条件满足时运行。这个表达式：

```text
0 0 13 * 5
```

并不是“黑色星期五”，而是“每月 13 号或每个星期五”，一年大约运行 60 次，而不是一两次。

实用规则：

- 只限制一个日期字段。
- 只有在 Quartz 等支持 ? 的调度器中才使用 ?。
- 需要同时满足两个条件时，可以每天运行并在命令里判断日期，或改用支持日历逻辑的调度器。

算一下就清楚了。一年有 12 个 13 号，加上大约 52 个星期五，共 64 个候选日；减去一两个恰好重合的月份，实际约 62 次。如果本意是“13 号且星期五”，这条表达式错了大约 30 倍。

这不是某个实现的 bug。POSIX 规定两个日期字段都受限时按“或”执行，Vixie cron 及其后代都照此实现。Quartz 反过来要求其中一个字段写 ?，所以从 Linux 复制过去的表达式可能直接被拒绝。两个字段都写 * 时每天都会匹配，或逻辑反而无害。

## 时区与夏令时

Cron 默认使用服务器本地时区，除非实现明确支持时区设置。这会带来两个问题：

- 不同区域的服务器会在不同的绝对时间运行同一份 crontab。
- 夏令时切换可能跳过或重复某个小时。

需要稳定时间的调度应优先使用 UTC。必须跟随本地时区的业务任务，应在调度器允许的地方显式设置时区，并测试切换日期。安排在本地时间 02:30 的任务，在时钟向前拨的那天可能不会运行。

看一个具体例子。美国东部时间 2026-03-08 凌晨 02:00 拨快到 03:00，02:00 到 02:59 这一小时不存在。America/New_York 时区里写 30 2 * * * 的任务，当天找不到对应的时刻：有的实现直接跳过，有的在 03:00 补跑，和原本 03:00 的任务撞在一起。到了 2026-11-01，01:00 到 01:59 重复一次，30 1 * * * 可能运行两遍，墙钟标签相同，对应的 UTC 时刻却不同。欧洲的切换日期是 2026-03-29 和 2026-10-25，被跳过的小时同样是 02:00 到 02:59。如果任务负责扣款、发提醒或轮换凭证，跑两遍通常比晚跑一遍更糟。

不同实现的行为不一样，所以测试很重要。Vixie cron 及其后代都会感知时钟变化，并有显式的 DST 处理，但具体结果随版本和发行版而变：被跳过区间里的任务可能在跳变后补跑一次，重复区间里的任务可能跑两遍。Debian cron、cronie 和 BusyBox cron 在这里并不等价。systemd timer 用日历语义计算下一次触发，并明确记录 DST 行为，但结果仍取决于 OnCalendar 表达式。较新的 Kubernetes CronJob 支持 timeZone，错过的运行则由 startingDeadlineSeconds 决定。稳妥的模式是：任务含义是绝对时间就用 UTC 调度；必须跟随本地营业时间时，让任务本身可幂等重试。

## Cron 不是间隔计时器

Cron 描述的是墙上时钟时间，不是经过的时间间隔。一个运行 40 分钟的任务如果每 30 分钟调度一次，就会重叠；安排在零点的任务也不会在夏令时切换后保持相同间隔。

如果需要“每 90 分钟”或“上次完成后 20 分钟”，Cron 不是合适的工具。应使用队列、工作循环，或支持间隔与并发控制的调度器。如果必须使用 Cron，应加锁，避免两次运行重叠。

重叠很容易想象。00:00 启动的任务要跑 40 分钟，00:30 Cron 又启动一次，此时第一次还没结束。两个进程写同一个输出文件、给同一个计数器加一，第二次还可能读到写了一半的状态。用 flock -n 加锁能让第二次立即退出，这通常比排队更好，但被跳过的运行必须可以接受。如果不能接受，说明用错了层次，应该换成队列。

systemd timer 提供折中方案：OnUnitActiveSec=90min 按上次激活时间计算，而不是按墙钟槽位；service 单元还可以限制同时只跑一个实例。在现代 Linux 上，这往往是不引入队列时最干净的修法，而且 systemctl list-timers 可以直接用来验证。

## 环境与路径差异

crontab 运行时的环境非常精简，常见故障包括：

- PATH 比交互式 shell 短，导致 command not found。
- 相对路径从错误的目录解析。
- .bashrc 或 .zshrc 里的环境变量没有被加载。
- 没有配置邮件，输出直接消失。

应使用绝对路径，在 crontab 或包装脚本中显式设置需要的变量，并把输出重定向到日志文件。

默认环境比多数人想象的更精简。PATH 通常是 /usr/bin:/bin，SHELL 是 /bin/sh，HOME 可能有值，而 LANG、NODE_ENV 和应用密钥都没有。交互式 shell 里能跑的命令，在 Cron 里可能因为装在 /usr/local/bin、缺少版本管理器 shim，或者工作目录是用户主目录而不是项目目录而失败。Cron 也不会读取 .bashrc 或 .zshrc，那些文件只服务于交互式 shell。

输出处理是任务的一部分。配置了邮件系统时，标准输出和标准错误会寄给用户；没有配置时，它们会被直接丢弃。应把两者重定向到带时间戳的日志文件，或者把命令包进脚本，写入 syslog 或结构化日志。否则故障的第一个信号会是监控面板数据不再更新，那已经太晚了。

## 部署前验证

1. 把表达式翻译成一句可读的调度描述。
2. 在目标时区下检查接下来十次运行时间。
3. 确认调度器方言：5 字段还是 6 字段。
4. 检查日期字段的行为。
5. 用相同的用户和环境手动执行命令。
6. 增加日志和失败告警。

[Cron 表达式生成器](/tools/cron)可以显示接下来的运行时间，并帮助把表达式翻译成自然语言。调度跨时区时，可以用 [Unix 时间戳转换工具](/tools/unix-timestamp)把时间换算成绝对时刻再比较。

两种验证手段针对不同错误。生成器确认表达式的含义，但不知道服务器时区。systemd-analyze calendar '*-*-* 02:30:00' 会打印 systemd 表达式的后续触发时间，并且显式处理 DST，应在切换日期前后各跑一次。经典 crontab 则可以把时间临时改到下一分钟，观察日志后再改回去。只有让真实调度器跑一次，才能发现环境和权限问题。

## Cron 的不同实现

并非所有 Cron 都是经典 Cron：

- **Vixie cron** 和多数 Linux crontab 使用五个字段，并采用前面说的“或”行为。
- **Quartz** 使用六个或七个字段，包含秒，并支持 ?、L 和 W。
- **Kubernetes CronJob** 使用五个字段，较新版本提供独立的 timeZone 字段。
- **云调度服务** 通常还提供时区、重试和补跑选项。

在不同系统之间直接复制表达式，很容易得到一个运行过于频繁或永远不运行的调度。

部署流水线里还有两种常见方言。GitHub Actions 使用五字段 POSIX 语法，但始终按 UTC 计算，最短间隔是五分钟；从本地时区服务器复制来的表达式会整体偏移一个时差。AWS EventBridge 使用带年份的六字段格式，要求月内日期或星期之一写 ?，并按调度配置的时区执行。Jenkins 的语法又不同，支持 H 散列值和独立的 @daily 形式。表达式看起来眼熟，背后的解析器却未必是你测试过的那一个。

## 部署检查清单

- 字段数量符合目标调度器。
- 只限制了一个日期字段。
- 时区明确且正确。
- 接下来的运行时间符合需求。
- 命令使用绝对路径。
- 输出写入日志。
- 已防止运行重叠。
- 失败会触发告警。
- 检查过 DST 切换日期，而不只是今天的下一次运行。
- 任务可幂等重试，或重复执行无害。

Cron 表达式是与时钟签订的契约。逐字段阅读，确认方言和时区，并在上线前验证接下来的运行时间。

遇到这些情况就不该用 Cron：任务必须严格每隔 N 分钟运行；单次运行经常超过间隔；触发条件取决于事件而不是时刻；宕机后错过的运行必须补上。此时应使用带可见性超时的队列、支持持久定时器的工作流引擎，或明确提供补跑语义的 systemd timer。Cron 只擅长一件事：在某个墙钟时刻启动一条短小、独立的命令。把它用在这件事上，其余需求交给能表达该需求的调度器。

## 常见问题

### */5 * * * * 是什么意思？

每 5 分钟运行一次。分钟字段是 */5，其余字段为通配符。它会在服务器本地时区的 0、5、10 等分钟触发。

### 为什么 0 0 13 * 5 比预期运行得更频繁？

经典 Cron 把月内日期和星期按“或”处理。这个表达式会在每月 13 号或每个星期五运行，而不是只在既是星期五又是 13 号时运行。需要同时满足两个条件时，应只保留一个日期字段，在命令里判断日期，或改用其他调度器。

### 如何每 90 分钟运行一次？

单条 Cron 无法干净地表达 90 分钟间隔，因为分钟和小时字段会在 24 小时周期内重复。可以用两条错开的表达式、带循环的包装脚本，或支持间隔语法的调度器。

### Cron 会处理夏令时吗？

Cron 跟随系统时钟。时钟向前拨时，落在被跳过小时内的任务可能不运行；时钟向后拨时，任务可能运行两次。关键任务应使用 UTC，或使用明确支持时区和夏令时的调度器。

### 5 字段和 6 字段 Cron 有什么区别？

5 字段 Cron 从分钟到星期。6 字段变体（例如 Quartz）通常在开头增加秒字段，有的还会增加年份字段。复制表达式前，先确认目标调度器的格式。

## 参考资料

- [crontab(5) - Linux 手册页](https://man7.org/linux/man-pages/man5/crontab.5.html)
- [cron - Arch Linux Wiki](https://wiki.archlinux.org/title/Cron)
- [systemd.timer 手册页](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html)
- [RFC 3339 - 互联网日期与时间](https://www.rfc-editor.org/rfc/rfc3339)

---

- Canonical: https://www.jsonmind.com/zh-CN/guides/cron-expression-guide
- Locale: zh-CN
- Published: 2026-11-10
- Updated: 2026-11-10
- Tools: cron, unix-timestamp