DevKitLab Logo DevKitLab
Cron / crontab / 定时任务 / 运维

Cron 定时任务怎么写?每分钟、每小时、每天、每周、每月速查

你已经知道要什么节奏——每 5 分钟、工作日 9 点、每月 1 号——只差那一行。这里按需求列出可直接复制的 cron 表达式,以及那几个直觉写法恰好是错的情况。

多数 cron 教程从表达式讲起,教你怎么读。这篇反过来:你已经能说清自己要什么——每五分钟、每个工作日 9 点、每月 1 号——只差一行可直接粘贴的表达式。下面按节奏分组列出常见写法。

复制时记住两点。大多数例子都是标准的五字段 crontab(分、时、日、月、星期),平台专属的写法会单独标明;想弄懂语法背后的「为什么」,看《怎么读懂 Cron 表达式》。有几个常见需求恰恰最容易凭直觉写错——「每 35 分钟」「每 30 秒」「每月第一个周一」「每月最后一天」——所以这里不只给表达式,也说明正确做法。

每 N 分钟

* * * * *      # 每分钟
*/5 * * * *    # 每 5 分钟
*/15 * * * *   # 每 15 分钟
*/30 * * * *   # 每 30 分钟

分钟位的 */N 意思是「每 N 分钟」,从 0 开始数:*/15 在 :00、:15、:30、:45 触发。只有当 N 能整除 60 时它才如你所愿——2、3、4、5、6、10、12、15、20、30。

*/35 不是每 35 分钟。步进从字段起点数、每小时重置,所以 */35 在 :00 和 :35 触发,然后小时翻篇,又从 :00 开始——35 分钟的间隔后面跟一个 25 分钟的,永远如此。凡是不能整除 60 的都这样(*/45 是 45 分接 15 分)。真要每 35 或每 90 分钟的节奏,cron 的分钟位表达不了,改用固定间隔的 systemd timer 或别的调度器。

每 N 小时

0 * * * *      # 每小时整点
0 */2 * * *    # 每 2 小时(00:00、02:00、04:00……)
0 */6 * * *    # 每 6 小时(00:00、06:00、12:00、18:00)
15 * * * *     # 每小时的第 15 分

分钟位放 0,任务才在整点触发一次,而不是那一小时里跑六十次——分钟留成 * 却去步进小时,是常见错误。「能整除」的规则同样适用于 24 小时范围:*/2*/3*/4*/6*/8*/12 均匀;*/5(0、5、10、15、20,然后跳回 0)不均匀。

每天定点

0 0 * * *      # 每天午夜(00:00)
30 6 * * *     # 每天 06:30
0 9 * * *      # 每天 09:00
0 22 * * *     # 每天 22:00(晚上 10 点)

先分钟、后小时,都是 24 小时制。30 6 是 06:30;反过来写 6 30 就报错,因为没有 30 点。字段顺序决定含义,不能调换。

一天多次

0 9,17 * * *     # 一天两次,09:00 和 17:00
0 0,12 * * *     # 一天两次,00:00 和 12:00
0 8-18 * * *     # 整点触发,08:00 到 18:00
0 9-17/2 * * *   # 09:00 到 17:00 每 2 小时(9、11、13、15、17)

逗号列举具体值;带步进的范围(9-17/2)在范围内按步长走。范围右端是的:8-18 含 18:00,9-17/2 含 17:00。

跨午夜的时间窗

0 22-23,0-6 * * *   # 每小时,22:00 到次日 06:00

范围只能往上数,所以 22-6 不是合法的跨夜窗口——cron 不会绕午夜回卷,具体看解析器,要么报错,要么什么都不匹配。拆成午夜两侧相接的两段:22-230-6。任何回卷都一样:「周五到周一」不是 5-1,而是 5,6,0,1

按星期

0 9 * * 1-5       # 工作日(周一到周五)09:00
0 9 * * 1         # 每周一 09:00
0 9 * * 0         # 每周日 09:00(0 = 周日)
0 9 * * 6,0       # 周末(周六 + 周日)09:00
0 9 * * MON-FRI   # 工作日,用名字写

:星期的数字不可移植。标准 cron 数 0–7,0 和 7 都是周日。但 Cloudflare Workers 从 1=周日 数到 7=周六,所以 1-5 在那儿是周日到周四,不是周一到周五;Kubernetes 把 0 当周日。凡是平台认名字的地方就用名字——MON-FRISUN——编号差异就不再咬人。(方言差异详见阅读指南。)

工作日营业时间内每 N 分钟

*/15 9-17 * * 1-5   # 每 15 分钟,09:00 到 17:xx,周一到周五
0 9-17 * * 1-5      # 整点,09:00 到 17:00,周一到周五

最常见的组合需求,从左往右读:每 15 分钟,在 9 到 17 点之间,工作日。有个要预期的细节:小时范围右端是含的,9-17 覆盖整个 17 点这一小时——任务会在 17:15、17:30、17:45 继续触发,直到 18:00 才停。要想 17:00 整点收尾,把步进范围收到 9-16,再补一行管 17 点整:0 17 * * 1-5

按月 / 具体几号

0 0 1 * *      # 每月 1 号午夜
0 0 15 * *     # 每月 15 号
0 9 1 1,7 *    # 1 月 1 号和 7 月 1 号 09:00(一年两次)
0 0 1 */3 *    # 每季度第一天(1、4、7、10 月)

这里有三个坑。第一,没有现成的字段表示「每月最后一天」——日是个死数字,而各月长短不一。L 能表示(0 0 L * *),但 L方言扩展(Quartz 等少数),不是标准 cron,用前先确认平台支持;可移植的兜底在下一节。

第二,日和星期按「或」组合:如果你同时限定了日和星期两个字段,标准 cron 是任一匹配就跑,不是两个都满足。所以 0 0 13 * FRI 意思是「每月 13 号,外加每个周五」——不是「黑色星期五」。除非你真要「或」,否则把日和星期里留一个是 *

第三,某个月没有的日子,那个月就干脆不触发。0 0 31 * * 只在有 31 天的七个月份触发——跳过 2、4、6、9、11 月——而 0 0 30 2 *(2 月 30 号)永远不触发。想要「月底」而不管月长,别把 30 或 31 写死,用下一节的守卫。

每月第 N 个星期几

「第一个周一」「最后一个周五」——月度报表和维护窗口常用,也正是「或」规则咬人的地方。直觉写的 0 9 1-7 * 1错的:日(1-7)和星期(1)两个字段都被限定,cron 会按「或」组合,于是它在 1 到 7 号每天跑、外加每个周一。可移植的写法是:调度日期窗口,把星期的判断放进任务里:

# 每月第一个周一 09:00 —— 1 到 7 号里必然只有一个周一
0 9 1-7 * *  [ "$(date +\%u)" = 1 ] && /srv/app/monthly.sh

date +%u 是 ISO 星期(1 = 周一……7 = 周日);这个守卫让任务只在 1–7 号里的那个周一才继续。注意 % 转义成了 \%——crontab 命令里没转义的 % 会让 cron 在这里截断命令(这个坑见《为什么我的 cron 没跑起来?》)。要「最后一个周五」,就每周五都跑,让脚本判断下一个周五是不是落在了下个月。如果你的平台是 Quartz,方言直接就能说:MON#1 是第一个周一,6L 是最后一个周五。

亚分钟:「每 30 秒」

标准 cron 的最细粒度是一分钟。没有秒字段,所以「每 30 秒」写不成五字段。有两种保持 cron 形式的变通法;如果可靠性重要,还有一种更合适的替代方案:

# 1. 六字段 cron(Spring、很多库)把秒放最前:
*/30 * * * * *              # 每 30 秒,前提是支持六字段

# 2. 两条五字段,其中一条用 sleep 错开:
* * * * *              /path/to/job
* * * * *  sleep 30 && /path/to/job

六字段只在认秒字段的调度器上有效——数一下字段,六个就说明第一个是秒——经典 Unix crontab 是五字段,会直接拒绝。sleep 30 这一对是给一分钟 crontab 的务实变通法,但不是精确间隔:两次调用彼此独立,单次任务超过 30 秒就可能重叠。真要稳定地按亚分钟运行,用带 OnUnitActiveSec=30ssystemd timer,或者一个自带循环的常驻进程,都比 cron 合适。

命名简写

Cronie / Vixie 式的 crontab 认几个命名简写,替代整个五字段:

@hourly    # 0 * * * *
@daily     # 0 0 * * *   (@midnight 一样)
@weekly    # 0 0 * * 0
@monthly   # 0 0 1 * *
@yearly    # 0 0 1 1 *   (@annually 一样)
@reboot    # 守护进程启动时,一次

读着顺,但它们是只有部分 crontab 支持的扩展——GitHub Actions、Cloudflare 这类托管调度器要五字段形式。@reboot 尤其需要一台有开机生命周期的机器,所以无服务器平台不会跑它。拿不准就用简写对应的那条显式五字段。

当字段表达不了时

有些按日历判断的需求写不进五字段:每月最后一天、每月第一个周一、除节假日外的每个工作日。通用做法是——调度得比你需要的更勤,再让任务自己判断要不要真跑。把 cron 说不出的条件放进包装脚本顶部的守卫,不到点就提前退出:

0 0 * * *  /srv/app/eom.sh      # 每天跑……
#!/bin/bash
# eom.sh —— 只在每月最后一天继续
[ "$(date -d tomorrow +%d)" = "01" ] || exit 0
# …… 真正的活儿 ……

其余需求都是同一套:每周五都跑,只有下一个周五落在下个月才继续(最后一个周五);每个工作日都跑,遇到节假日清单里的日期就退出。像每 90 分钟这样的固定间隔,不要硬塞进按小时触发的守卫里,改用固定间隔调度器。cron 负责在合适的时间窗口触发,脚本做最后判断。(date -d 是 GNU coreutils;BSD 或 macOS 用 date -v+1d +%d。)

交付前验证

上面的五字段表达式都属于标准 cron;本文也特意标出了 L、秒字段和命名简写这些平台相关的扩展。即使同一条五字段表达式,在不同平台也可能意思不同——例如星期编号和运行时区。两个习惯能挡掉大多数错误:

  • 丢进工具再读一遍。把表达式粘进 Cron 表达式工具:它用大白话打印调度,并按你选的时区列出接下来的运行时间,「工作日每 15 分钟」是核对出来的,不是猜的。(@daily 这类简写先展开成五字段——工具分析的是数字字段,不是宏。)
  • 盯住时区。cron 行自己不带时区;0 9 * * * 是「9:00」,具体几点看运行时用哪个时区——服务器本地,或多数托管平台用的 UTC。这点要紧的话,工具会按你选的时区显示每次运行;要把服务器与自己的时区偏移对齐,用时区转换工具《UTC、GMT、ISO 8601 与 Unix 时间戳有什么区别?》再替你理清背后的原因。

而要是一行你已确认没写错、跑起来却什么都没发生,那是另一半故事——环境、crontab 放的位置、或这台机器本身——都在《为什么我的 cron 没跑起来?》里。

快查表

你想要那一行
每分钟* * * * *
每 5 分钟*/5 * * * *
每 15 分钟*/15 * * * *
每小时整点0 * * * *
每 2 小时0 */2 * * *
每天午夜0 0 * * *
每天 09:000 9 * * *
一天两次(09:00、17:00)0 9,17 * * *
跨夜每小时(22:00–06:00)0 22-23,0-6 * * *
工作日 09:000 9 * * 1-5
每周一 09:000 9 * * 1
周末 09:000 9 * * 6,0
营业时间每 15 分钟*/15 9-17 * * 1-5
每月 1 号午夜0 0 1 * *
每季度第一天0 0 1 */3 *
每月第一个周一 09:000 9 1-7 * * + 星期守卫
每月最后一天每天跑 + date 守卫